├── engine ├── src │ ├── __init__.py │ └── api │ │ ├── utils │ │ ├── __init__.py │ │ ├── clock.py │ │ └── helpers.py │ │ ├── __about__.py │ │ ├── __init__.py │ │ ├── integrations │ │ └── jenkins │ │ │ ├── secret.yaml │ │ │ ├── __init__.py │ │ │ └── jenkins.py │ │ ├── config │ │ ├── rate_limit.py │ │ ├── __init__.py │ │ ├── database.py │ │ └── settings.py │ │ ├── agent │ │ ├── stop.py │ │ └── state.py │ │ ├── middleware │ │ ├── __init__.py │ │ └── logging_middleware.py │ │ ├── schemas │ │ └── team.py │ │ ├── models │ │ ├── __init__.py │ │ ├── integration.py │ │ ├── user.py │ │ └── conversation.py │ │ ├── endpoints │ │ ├── __init__.py │ │ ├── health.py │ │ └── integrations.py │ │ ├── services │ │ ├── approvals.py │ │ ├── limiter.py │ │ ├── stop_service.py │ │ ├── checkpointer.py │ │ ├── tools_cache.py │ │ └── title_generator.py │ │ └── asgi.py ├── .python-version ├── aerich.ini ├── migrations │ └── models │ │ ├── 1_20250912203310_update.py │ │ └── 0_20250903153104_init.py ├── .env.example ├── alembic.ini ├── pyproject.toml └── .gitignore ├── mcp ├── .python-version ├── tests │ ├── __init__.py │ ├── tools │ │ ├── __init__.py │ │ ├── test_helm.py │ │ ├── test_argo.py │ │ └── test_jenkins.py │ └── utils │ │ ├── __init__.py │ │ └── test_commands.py ├── tools │ └── __init__.py ├── __about__.py ├── pytest.ini ├── utils │ ├── types.py │ └── commands.py ├── .env.example ├── config │ └── server.py ├── main.py ├── run_tests.sh ├── pyproject.toml └── .gitignore ├── assets └── readme.png ├── ui ├── src │ ├── app │ │ ├── icon.ico │ │ ├── favicon.ico │ │ ├── page.tsx │ │ ├── chat │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── history │ │ │ └── page.tsx │ │ ├── integrations │ │ │ └── page.tsx │ │ ├── settings │ │ │ └── page.tsx │ │ ├── api │ │ │ ├── auth │ │ │ │ ├── me │ │ │ │ │ └── route.ts │ │ │ │ └── admin-check │ │ │ │ │ └── route.ts │ │ │ ├── integrations │ │ │ │ ├── route.ts │ │ │ │ └── [id] │ │ │ │ │ └── route.ts │ │ │ ├── profile │ │ │ │ └── route.ts │ │ │ └── conversation │ │ │ │ ├── route.ts │ │ │ │ └── [id] │ │ │ │ └── route.ts │ │ ├── layout.tsx │ │ ├── welcome │ │ │ └── page.tsx │ │ ├── login │ │ │ └── page.tsx │ │ └── not-found.tsx │ ├── components │ │ ├── ui │ │ │ ├── skeleton.tsx │ │ │ ├── label.tsx │ │ │ ├── input.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── ToastContainer.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── code-block.tsx │ │ │ ├── button.tsx │ │ │ └── card.tsx │ │ ├── Layout.tsx │ │ ├── chat │ │ │ ├── index.ts │ │ │ ├── ChatHeader.tsx │ │ │ ├── QueuedMessagesBar.tsx │ │ │ ├── ChatSuggestions.tsx │ │ │ ├── PendingApprovalsBar.tsx │ │ │ └── Welcome.tsx │ │ ├── Header.tsx │ │ ├── auth │ │ │ ├── AuthInput.tsx │ │ │ ├── Login.tsx │ │ │ └── Register.tsx │ │ ├── settings │ │ │ └── Settings.tsx │ │ └── navbar │ │ │ └── Navbar.tsx │ ├── types │ │ ├── auth.ts │ │ ├── chat.ts │ │ └── events.ts │ ├── lib │ │ ├── utils.ts │ │ ├── debounce.ts │ │ ├── api.ts │ │ └── approvals.ts │ └── store │ │ └── useAuthStore.ts ├── public │ ├── logo_transparent.png │ └── logo_vector_transparent.png ├── postcss.config.mjs ├── next.config.mjs ├── .eslintrc.json ├── .env.example ├── components.json ├── .gitignore ├── tsconfig.json ├── tailwind.config.ts └── package.json ├── .gitignore ├── deployment ├── mcp │ ├── entrypoint.sh │ └── Dockerfile ├── ui │ ├── proxy.Dockerfile │ ├── nginx.conf │ └── Dockerfile ├── local.kind.yaml ├── engine │ ├── entrypoint.sh │ └── Dockerfile ├── kubernetes-controller │ └── Dockerfile ├── local.test-deploy.yaml ├── local.docker-compose.yaml ├── uninstall.sh └── README.md ├── kubernetes-controller ├── .gitignore ├── hack │ └── boilerplate.go.txt ├── config │ └── rbac │ │ └── role.yaml ├── engine │ └── v1 │ │ └── groupversion_info.go ├── cmd │ └── manager │ │ └── main.go └── go.mod ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── discord-gfi.yml ├── CONTRIBUTING.md └── README.md /engine/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mcp/.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /engine/.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /engine/src/api/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /engine/src/api/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.0" 2 | -------------------------------------------------------------------------------- /mcp/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests package for MCP module.""" 2 | -------------------------------------------------------------------------------- /mcp/tests/tools/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for tools module.""" 2 | -------------------------------------------------------------------------------- /mcp/tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for utils module.""" 2 | -------------------------------------------------------------------------------- /mcp/tools/__init__.py: -------------------------------------------------------------------------------- 1 | """Tools package for Skyflo.ai MCP Server.""" 2 | -------------------------------------------------------------------------------- /mcp/__about__.py: -------------------------------------------------------------------------------- 1 | """Version information.""" 2 | 3 | __version__ = "0.2.0" 4 | -------------------------------------------------------------------------------- /assets/readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyflo-ai/skyflo/HEAD/assets/readme.png -------------------------------------------------------------------------------- /ui/src/app/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyflo-ai/skyflo/HEAD/ui/src/app/icon.ico -------------------------------------------------------------------------------- /ui/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyflo-ai/skyflo/HEAD/ui/src/app/favicon.ico -------------------------------------------------------------------------------- /engine/src/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .__about__ import __version__ 2 | 3 | __all__ = ["__version__"] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_store 3 | .venv 4 | .mypy_cache 5 | .idea 6 | .vscode/ 7 | *.sw? 8 | .cursor 9 | -------------------------------------------------------------------------------- /ui/public/logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyflo-ai/skyflo/HEAD/ui/public/logo_transparent.png -------------------------------------------------------------------------------- /ui/public/logo_vector_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyflo-ai/skyflo/HEAD/ui/public/logo_vector_transparent.png -------------------------------------------------------------------------------- /engine/aerich.ini: -------------------------------------------------------------------------------- 1 | [aerich] 2 | tortoise_orm = src.api.config.database.TORTOISE_ORM_CONFIG 3 | location = ./migrations 4 | src_folder = ./src/ -------------------------------------------------------------------------------- /deployment/mcp/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Starting Skyflo.ai MCP Server..." 5 | exec python main.py --host 0.0.0.0 --port 8888 -------------------------------------------------------------------------------- /ui/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /mcp/pytest.ini: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = tests 3 | python_files = test_*.py 4 | python_classes = Test* 5 | python_functions = test_* 6 | addopts = -v --tb=short 7 | asyncio_mode = auto 8 | -------------------------------------------------------------------------------- /deployment/ui/proxy.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.25-alpine 2 | 3 | # Copy nginx configuration 4 | COPY deployment/ui/nginx.conf /etc/nginx/conf.d/default.conf 5 | 6 | EXPOSE 80 7 | 8 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /ui/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | eslint: { 4 | ignoreDuringBuilds: true, 5 | }, 6 | output: "standalone", 7 | }; 8 | 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /engine/src/api/integrations/jenkins/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {name} 5 | namespace: {namespace} 6 | type: Opaque 7 | stringData: 8 | username: {username} 9 | api-token: {api_token} 10 | -------------------------------------------------------------------------------- /mcp/utils/types.py: -------------------------------------------------------------------------------- 1 | """Type definitions for MCP tools.""" 2 | 3 | from typing_extensions import TypedDict 4 | 5 | 6 | class ToolOutput(TypedDict): 7 | """Structured output from tool commands.""" 8 | 9 | output: str 10 | error: bool 11 | -------------------------------------------------------------------------------- /ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:@typescript-eslint/recommended" 5 | ], 6 | "parser": "@typescript-eslint/parser", 7 | "plugins": ["@typescript-eslint"], 8 | "rules": { 9 | // Add any custom rules here 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface SkeletonProps { 4 | className?: string; 5 | } 6 | 7 | export function Skeleton({ className }: SkeletonProps) { 8 | return ( 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /engine/src/api/config/rate_limit.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends 2 | from fastapi_limiter.depends import RateLimiter 3 | 4 | from . import settings 5 | 6 | 7 | rate_limit_dependency = ( 8 | Depends(RateLimiter(times=settings.RATE_LIMIT_PER_MINUTE, seconds=60)) 9 | if settings.RATE_LIMITING_ENABLED 10 | else None 11 | ) 12 | -------------------------------------------------------------------------------- /mcp/.env.example: -------------------------------------------------------------------------------- 1 | # MCP Server Settings 2 | APP_NAME=Skyflo.ai - MCP Server 3 | APP_VERSION=0.4.0 4 | APP_DESCRIPTION=MCP Server for Skyflo.ai 5 | DEBUG=false 6 | 7 | # Retry Configuration 8 | MAX_RETRY_ATTEMPTS=3 9 | RETRY_BASE_DELAY=60 10 | RETRY_MAX_DELAY=300 11 | RETRY_EXPONENTIAL_BASE=2.0 12 | 13 | # Server Settings 14 | LOG_LEVEL=INFO -------------------------------------------------------------------------------- /ui/.env.example: -------------------------------------------------------------------------------- 1 | # UI Settings 2 | NEXT_PUBLIC_APP_NAME=Skyflo.ai 3 | NEXT_PUBLIC_APP_VERSION=0.4.0 4 | NODE_ENV=production 5 | 6 | # API Connection 7 | API_URL=http://skyflo-ai-engine:8080/api/v1 8 | NEXT_PUBLIC_API_URL=http://skyflo-ai-engine:8080/api/v1 9 | 10 | # Feature Flags 11 | NEXT_PUBLIC_ENABLE_ANALYTICS=false 12 | NEXT_PUBLIC_ENABLE_FEEDBACK=false -------------------------------------------------------------------------------- /ui/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import Header from "./Header"; 3 | 4 | interface LayoutProps { 5 | children: ReactNode; 6 | } 7 | 8 | export default function Layout({ children }: LayoutProps) { 9 | return ( 10 |
11 |
12 |
{children}
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /engine/src/api/agent/stop.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | 3 | from ..utils.helpers import get_state_value 4 | from ..services.stop_service import should_stop 5 | 6 | 7 | class StopRequested(Exception): 8 | pass 9 | 10 | 11 | async def check_stop(state: Dict[str, Any]) -> None: 12 | run_id = get_state_value(state, "run_id") 13 | if await should_stop(run_id): 14 | raise StopRequested() 15 | -------------------------------------------------------------------------------- /engine/src/api/integrations/jenkins/__init__.py: -------------------------------------------------------------------------------- 1 | from .jenkins import ( 2 | build_jenkins_secret_yaml, 3 | filter_jenkins_tools, 4 | inject_jenkins_metadata_tool_args, 5 | strip_jenkins_metadata_tool_args, 6 | ) 7 | 8 | __all__ = [ 9 | "build_jenkins_secret_yaml", 10 | "filter_jenkins_tools", 11 | "inject_jenkins_metadata_tool_args", 12 | "strip_jenkins_metadata_tool_args", 13 | ] 14 | -------------------------------------------------------------------------------- /ui/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Navbar from "@/components/navbar/Navbar"; 5 | import { Welcome } from "@/components/chat"; 6 | 7 | export default function HomePage() { 8 | return ( 9 |
10 | 11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /engine/src/api/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .settings import settings, get_settings 2 | from .rate_limit import rate_limit_dependency 3 | from .database import init_db, close_db_connection, generate_schemas, get_tortoise_config 4 | 5 | __all__ = [ 6 | "settings", 7 | "get_settings", 8 | "rate_limit_dependency", 9 | "init_db", 10 | "close_db_connection", 11 | "generate_schemas", 12 | "get_tortoise_config", 13 | ] 14 | -------------------------------------------------------------------------------- /deployment/local.kind.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kind.x-k8s.io/v1alpha4 2 | kind: Cluster 3 | name: skyflo-ai 4 | nodes: 5 | - role: control-plane 6 | image: kindest/node:v1.32.2 7 | extraPortMappings: 8 | - containerPort: 30080 9 | hostPort: 30080 10 | listenAddress: "0.0.0.0" 11 | protocol: TCP 12 | - containerPort: 30081 13 | hostPort: 30081 14 | listenAddress: "0.0.0.0" 15 | protocol: TCP 16 | - role: worker 17 | image: kindest/node:v1.32.2 18 | -------------------------------------------------------------------------------- /ui/src/app/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ChatInterface } from "@/components/chat"; 4 | import Navbar from "@/components/navbar/Navbar"; 5 | 6 | export default function ChatPage({ params }: { params: { id: string } }) { 7 | return ( 8 |
9 | 10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /ui/src/app/history/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Navbar from "@/components/navbar/Navbar"; 5 | import History from "@/components/History"; 6 | 7 | export default function HistoryPage() { 8 | return ( 9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/integrations/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Navbar from "@/components/navbar/Navbar"; 5 | import Integrations from "@/components/Integrations"; 6 | 7 | export default function IntegrationsPage() { 8 | return ( 9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/components/chat/index.ts: -------------------------------------------------------------------------------- 1 | export { ChatInterface } from "./ChatInterface"; 2 | export { ChatMessages } from "./ChatMessages"; 3 | export { ChatInput } from "./ChatInput"; 4 | export { ToolVisualization } from "./ToolVisualization"; 5 | export { PendingApprovalsBar } from "./PendingApprovalsBar"; 6 | export { QueuedMessagesBar } from "./QueuedMessagesBar"; 7 | export { TokenUsageDisplay } from "./TokenUsageDisplay"; 8 | export { Welcome } from "./Welcome"; 9 | export * from "../../types/chat"; 10 | 11 | export type { Suggestion } from "../../types/chat"; 12 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # IDE 39 | .idea/ 40 | .vscode/ 41 | .cursor/ -------------------------------------------------------------------------------- /kubernetes-controller/.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env -------------------------------------------------------------------------------- /kubernetes-controller/hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Skyflo.ai. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /ui/src/types/auth.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | email: string; 4 | full_name: string; 5 | role: string; 6 | is_active: boolean; 7 | is_superuser: boolean; 8 | is_verified: boolean; 9 | created_at: string; 10 | } 11 | 12 | export interface TeamMember { 13 | id: string; 14 | email: string; 15 | name: string; 16 | role: string; 17 | status: string; // "active", "pending", etc. 18 | created_at: string; 19 | } 20 | 21 | export interface AuthToken { 22 | access_token: string; 23 | token_type: string; 24 | } 25 | 26 | export interface AuthState { 27 | user: User | null; 28 | token: string | null; 29 | isLoading: boolean; 30 | isAuthenticated: boolean; 31 | } 32 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": false, 7 | "noImplicitAny": false, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /deployment/engine/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Wait for 30 seconds for the database to be ready 5 | sleep 30 6 | 7 | # Initialize Aerich if not already initialized 8 | if [ ! -d "migrations" ]; then 9 | echo "Initializing Aerich..." 10 | aerich init -t src.api.config.database.TORTOISE_ORM_CONFIG 11 | fi 12 | 13 | # Create initial migration if none exists 14 | if [ ! "$(ls -A migrations/models 2>/dev/null)" ]; then 15 | echo "Creating initial migration..." 16 | aerich init-db 17 | else 18 | # Apply any pending migrations 19 | echo "Applying pending migrations..." 20 | aerich upgrade 21 | fi 22 | 23 | # Start the API service with Uvicorn 24 | echo "Starting Engine service..." 25 | exec uvicorn src.api.asgi:app --host 0.0.0.0 --port 8080 -------------------------------------------------------------------------------- /ui/src/app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { useAuth } from "@/components/auth/AuthProvider"; 5 | import Navbar from "@/components/navbar/Navbar"; 6 | import { useAuthStore } from "@/store/useAuthStore"; 7 | import ProfileSettings from "@/components/settings/Settings"; 8 | 9 | export default function SettingsPage() { 10 | const { user } = useAuth(); 11 | const { user: storeUser } = useAuthStore(); 12 | 13 | return ( 14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /engine/src/api/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | """API middleware package.""" 2 | 3 | from fastapi import FastAPI 4 | from fastapi.middleware.cors import CORSMiddleware 5 | from .logging_middleware import LoggingMiddleware 6 | 7 | 8 | def setup_middleware(app: FastAPI) -> None: 9 | """Set up all middleware for the application.""" 10 | app.add_middleware( 11 | CORSMiddleware, 12 | allow_origins=[ 13 | "http://localhost:3000", 14 | "http://127.0.0.1:3000", 15 | "http://localhost:3001", 16 | ], 17 | allow_credentials=True, 18 | allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], 19 | allow_headers=["*"], 20 | ) 21 | 22 | app.add_middleware(LoggingMiddleware) 23 | 24 | 25 | __all__ = ["setup_middleware"] 26 | -------------------------------------------------------------------------------- /mcp/config/server.py: -------------------------------------------------------------------------------- 1 | from fastmcp import FastMCP 2 | 3 | mcp = FastMCP( 4 | "Skyflo.ai MCP Server", 5 | instructions=""" 6 | # Kubernetes DevOps MCP 7 | 8 | This MCP allows you to: 9 | 1. Manage Kubernetes clusters, resources, and deployments using kubectl operations 10 | 2. Install and manage applications with Helm charts and repositories 11 | 3. Execute progressive deployments with Argo Rollouts (blue/green, canary strategies) 12 | 4. Troubleshoot and diagnose cluster issues with comprehensive validation 13 | """, 14 | dependencies=[ 15 | "pydantic", 16 | "kubernetes", 17 | "helm", 18 | "argo", 19 | ], 20 | ) 21 | 22 | from tools import kubectl 23 | from tools import helm 24 | from tools import argo 25 | from tools import jenkins 26 | -------------------------------------------------------------------------------- /deployment/kubernetes-controller/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.24.1-alpine AS builder 3 | 4 | WORKDIR /workspace 5 | 6 | # Copy Go module files first for better layer caching 7 | COPY kubernetes-controller/go.mod kubernetes-controller/go.sum ./ 8 | RUN go mod download 9 | 10 | # Copy the source code 11 | COPY kubernetes-controller/ ./ 12 | 13 | # Build the controller binary 14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager cmd/manager/main.go 15 | 16 | # Runtime stage 17 | FROM gcr.io/distroless/static:nonroot 18 | 19 | WORKDIR / 20 | 21 | # Copy the controller binary from builder stage 22 | COPY --from=builder /workspace/manager /manager 23 | 24 | # Use nonroot user for security 25 | USER 65532:65532 26 | 27 | # Set entrypoint to the manager binary 28 | ENTRYPOINT ["/manager"] 29 | -------------------------------------------------------------------------------- /engine/src/api/schemas/team.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr, Field 2 | from typing import Optional 3 | 4 | 5 | class TeamMemberCreate(BaseModel): 6 | email: EmailStr 7 | role: str = Field(default="member", description="Role for the new user") 8 | password: str = Field(description="Initial password for the new user") 9 | 10 | 11 | class TeamMemberUpdate(BaseModel): 12 | role: str = Field(description="Updated role for the user") 13 | 14 | 15 | class TeamMemberRead(BaseModel): 16 | id: str 17 | email: str 18 | name: str 19 | role: str 20 | status: str # "active", "pending", "inactive" 21 | created_at: str 22 | 23 | 24 | class TeamInvitationRead(BaseModel): 25 | id: str 26 | email: str 27 | role: str 28 | created_at: str 29 | expires_at: Optional[str] = None 30 | -------------------------------------------------------------------------------- /deployment/local.test-deploy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: prod 6 | --- 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | metadata: 10 | name: api-prod 11 | namespace: prod 12 | labels: 13 | app: api-prod 14 | spec: 15 | replicas: 2 16 | selector: 17 | matchLabels: 18 | app: api-prod 19 | template: 20 | metadata: 21 | labels: 22 | app: api-prod 23 | spec: 24 | containers: 25 | - name: api-prod 26 | image: nginx:1.345 # Incorrect image tag 27 | ports: 28 | - containerPort: 80 29 | --- 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | name: api-prod 34 | namespace: prod 35 | spec: 36 | selector: 37 | app: api-prod 38 | ports: 39 | - protocol: TCP 40 | port: 80 41 | targetPort: 80 42 | type: ClusterIP 43 | -------------------------------------------------------------------------------- /ui/src/app/api/auth/me/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getAuthHeaders } from "@/lib/api"; 3 | 4 | export async function GET() { 5 | try { 6 | const headers = await getAuthHeaders(); 7 | 8 | const response = await fetch(`${process.env.API_URL}/auth/me`, { 9 | method: "GET", 10 | headers, 11 | cache: "no-store", 12 | }); 13 | 14 | if (!response.ok) { 15 | return NextResponse.json( 16 | { status: "error", error: "Failed to fetch user data" }, 17 | { status: response.status } 18 | ); 19 | } 20 | 21 | const data = await response.json(); 22 | return NextResponse.json(data); 23 | } catch (error) { 24 | return NextResponse.json( 25 | { status: "error", error: "Failed to fetch user data" }, 26 | { status: 500 } 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /engine/src/api/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .user import User 2 | from .conversation import Conversation, Message 3 | from .integration import Integration 4 | 5 | from .user import UserCreate, UserRead, UserUpdate, UserDB 6 | from .conversation import ( 7 | ConversationCreate, 8 | ConversationRead, 9 | ConversationUpdate, 10 | MessageCreate, 11 | MessageRead, 12 | ) 13 | from .integration import IntegrationCreate, IntegrationRead, IntegrationUpdate 14 | 15 | __all__ = [ 16 | "User", 17 | "Conversation", 18 | "Message", 19 | "Integration", 20 | "UserCreate", 21 | "UserRead", 22 | "UserUpdate", 23 | "UserDB", 24 | "ConversationCreate", 25 | "ConversationRead", 26 | "ConversationUpdate", 27 | "MessageCreate", 28 | "MessageRead", 29 | "IntegrationCreate", 30 | "IntegrationRead", 31 | "IntegrationUpdate", 32 | ] 33 | -------------------------------------------------------------------------------- /engine/migrations/models/1_20250912203310_update.py: -------------------------------------------------------------------------------- 1 | from tortoise import BaseDBAsyncClient 2 | 3 | 4 | async def upgrade(db: BaseDBAsyncClient) -> str: 5 | return """ 6 | CREATE TABLE IF NOT EXISTS "integrations" ( 7 | "id" UUID NOT NULL PRIMARY KEY, 8 | "provider" VARCHAR(50) NOT NULL UNIQUE, 9 | "name" VARCHAR(100), 10 | "metadata" JSONB, 11 | "credentials_ref" VARCHAR(255) NOT NULL, 12 | "status" VARCHAR(20) NOT NULL DEFAULT 'active', 13 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | "user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE, 16 | CONSTRAINT "uid_integration_provide_bb1a7e" UNIQUE ("provider") 17 | );""" 18 | 19 | 20 | async def downgrade(db: BaseDBAsyncClient) -> str: 21 | return """ 22 | DROP TABLE IF EXISTS "integrations";""" 23 | -------------------------------------------------------------------------------- /engine/src/api/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from .health import router as health_router 4 | from .agent import router as agent_router 5 | from .conversation import router as conversation_router 6 | from .auth import router as auth_router 7 | from .team import router as team_router 8 | from .integrations import router as integrations_router 9 | 10 | api_router = APIRouter() 11 | 12 | api_router.include_router(health_router, prefix="/health", tags=["health"]) 13 | api_router.include_router(agent_router, prefix="/agent", tags=["agent"]) 14 | api_router.include_router(conversation_router, prefix="/conversations", tags=["conversations"]) 15 | api_router.include_router(auth_router, prefix="/auth", tags=["auth"]) 16 | api_router.include_router(team_router, prefix="/team", tags=["team"]) 17 | api_router.include_router(integrations_router, prefix="/integrations", tags=["integrations"]) 18 | 19 | 20 | __all__ = ["api_router"] 21 | -------------------------------------------------------------------------------- /ui/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | } 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /ui/src/app/api/auth/admin-check/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getAuthHeaders } from "@/lib/api"; 3 | 4 | export async function GET() { 5 | try { 6 | const headers = await getAuthHeaders(); 7 | 8 | const apiUrl = process.env.API_URL; 9 | const response = await fetch(`${apiUrl}/auth/is_admin_user`, { 10 | method: "GET", 11 | headers: { 12 | "Content-Type": "application/json", 13 | ...headers, 14 | }, 15 | cache: "no-store", 16 | }); 17 | 18 | if (!response.ok) { 19 | return NextResponse.json( 20 | { status: "error", error: "Failed to check admin status" }, 21 | { status: response.status } 22 | ); 23 | } 24 | 25 | const data = await response.json(); 26 | return NextResponse.json(data); 27 | } catch (error) { 28 | return NextResponse.json( 29 | { status: "error", error: "Failed to check admin status" }, 30 | { status: 500 } 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { Button } from '@/components/ui/button' 3 | 4 | export default function Header() { 5 | return ( 6 |
7 | 20 |
21 | ) 22 | } 23 | 24 | -------------------------------------------------------------------------------- /engine/src/api/utils/clock.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import time 3 | from datetime import datetime, timezone 4 | from typing import Optional 5 | 6 | 7 | def now_ms() -> int: 8 | """Wall-clock epoch time in whole milliseconds (UTC).""" 9 | return time.time_ns() // 1_000_000 10 | 11 | 12 | def now_ns() -> int: 13 | """Wall-clock epoch time in nanoseconds (UTC).""" 14 | return time.time_ns() 15 | 16 | 17 | def now_iso_ms() -> str: 18 | """ISO-8601 UTC with millisecond precision (e.g., 2025-08-30T06:59:12.123Z).""" 19 | return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") 20 | 21 | 22 | def monotonic_start_ns() -> int: 23 | """Monotonic reference point for measuring durations.""" 24 | return time.monotonic_ns() 25 | 26 | 27 | def since_ms(start_ns: int, end_ns: Optional[int] = None) -> float: 28 | """Elapsed milliseconds using a monotonic clock.""" 29 | end = end_ns if end_ns is not None else time.monotonic_ns() 30 | return (end - start_ns) / 1_000_000.0 31 | -------------------------------------------------------------------------------- /engine/src/api/endpoints/health.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, Any 3 | 4 | from fastapi import APIRouter 5 | from tortoise import Tortoise 6 | 7 | from api.__about__ import __version__ 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | router = APIRouter() 12 | 13 | 14 | @router.get("/", tags=["health"]) 15 | async def health_check() -> Dict[str, Any]: 16 | return { 17 | "status": "ok", 18 | "version": __version__, 19 | } 20 | 21 | 22 | @router.get("/database", tags=["health"]) 23 | async def database_health_check() -> Dict[str, Any]: 24 | try: 25 | conn = Tortoise.get_connection("default") 26 | 27 | await conn.execute_query("SELECT 1") 28 | 29 | return { 30 | "status": "ok", 31 | "database": "connected", 32 | } 33 | except Exception as e: 34 | logger.exception("Database health check failed") 35 | return { 36 | "status": "error", 37 | "database": "disconnected", 38 | "error": str(e), 39 | } 40 | -------------------------------------------------------------------------------- /mcp/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | 4 | from config.server import mcp 5 | 6 | logging.basicConfig( 7 | level=logging.INFO, 8 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 9 | ) 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def main(): 14 | """Run the MCP server with HTTP transport.""" 15 | parser = argparse.ArgumentParser( 16 | description="Skyflo.ai MCP Server for cloud-native operations through natural language" 17 | ) 18 | parser.add_argument( 19 | "--port", type=int, default=8888, help="Port to run the server on" 20 | ) 21 | parser.add_argument( 22 | "--host", type=str, default="0.0.0.0", help="Host to bind the server to" 23 | ) 24 | 25 | args = parser.parse_args() 26 | 27 | logger.info( 28 | f"Starting Skyflo.ai MCP Server on {args.host}:{args.port} with HTTP transport" 29 | ) 30 | mcp.settings.port = args.port 31 | mcp.settings.host = args.host 32 | mcp.run(transport="http") 33 | 34 | 35 | if __name__ == "__main__": 36 | main() 37 | -------------------------------------------------------------------------------- /kubernetes-controller/config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - apps 9 | resources: 10 | - deployments 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - secrets 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - services 31 | verbs: 32 | - create 33 | - delete 34 | - get 35 | - list 36 | - patch 37 | - update 38 | - watch 39 | - apiGroups: 40 | - skyflo.ai 41 | resources: 42 | - skyfloais 43 | verbs: 44 | - create 45 | - delete 46 | - get 47 | - list 48 | - patch 49 | - update 50 | - watch 51 | - apiGroups: 52 | - skyflo.ai 53 | resources: 54 | - skyfloais/finalizers 55 | verbs: 56 | - update 57 | - apiGroups: 58 | - skyflo.ai 59 | resources: 60 | - skyfloais/status 61 | verbs: 62 | - get 63 | - patch 64 | - update 65 | -------------------------------------------------------------------------------- /ui/src/components/chat/ChatHeader.tsx: -------------------------------------------------------------------------------- 1 | const ChatHeader = () => { 2 | return ( 3 |
4 |
8 |
9 |
10 | 11 |
12 |

13 | I'm{" "} 14 | 15 | 16 | Sky 17 | 18 | 19 |

20 |

21 | Your DevOps Copilot for Kubernetes & Jenkins 22 |

23 |
24 |
25 | ); 26 | }; 27 | 28 | export default ChatHeader; 29 | -------------------------------------------------------------------------------- /engine/src/api/agent/state.py: -------------------------------------------------------------------------------- 1 | import time 2 | import uuid 3 | import operator 4 | from typing import Any, Dict, List, Optional, Annotated 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | class AgentState(BaseModel): 9 | run_id: str = Field(default_factory=lambda: str(uuid.uuid4())) 10 | messages: Annotated[List[Dict[str, Any]], operator.add] = Field(default_factory=list) 11 | pending_tools: List[Dict[str, Any]] = Field(default_factory=list) 12 | start_time: float = Field(default_factory=time.time) 13 | end_time: float = 0.0 14 | duration: float = 0.0 15 | done: bool = False 16 | conversation_id: Optional[str] = None 17 | user_id: Optional[str] = None 18 | error: Optional[str] = None 19 | retry_count: int = 0 20 | max_retries: int = 3 21 | auto_continue_turns: int = 0 22 | awaiting_approval: bool = False 23 | suppress_pending_event: bool = False 24 | ttft_emitted: bool = False 25 | approval_decisions: Dict[str, bool] = Field(default_factory=dict) 26 | 27 | class Config: 28 | arbitrary_types_allowed = True 29 | -------------------------------------------------------------------------------- /deployment/local.docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | name: skyflo_ai 3 | 4 | services: 5 | postgres: 6 | image: postgres:15-alpine 7 | container_name: skyflo_ai_postgres 8 | environment: 9 | - POSTGRES_USER=skyflo 10 | - POSTGRES_PASSWORD=skyflo 11 | - POSTGRES_DB=skyflo 12 | ports: 13 | - "5432:5432" 14 | volumes: 15 | - postgres_data:/var/lib/postgresql/data 16 | healthcheck: 17 | test: ["CMD-SHELL", "pg_isready -U skyflo"] 18 | interval: 10s 19 | timeout: 5s 20 | retries: 5 21 | start_period: 10s 22 | restart: unless-stopped 23 | 24 | redis: 25 | image: redis:7-alpine 26 | container_name: skyflo_ai_redis 27 | ports: 28 | - "6379:6379" 29 | volumes: 30 | - redis_data:/data 31 | healthcheck: 32 | test: ["CMD", "redis-cli", "ping"] 33 | interval: 10s 34 | timeout: 5s 35 | retries: 5 36 | start_period: 10s 37 | restart: unless-stopped 38 | command: redis-server --appendonly yes 39 | 40 | volumes: 41 | postgres_data: 42 | redis_data: 43 | -------------------------------------------------------------------------------- /engine/src/api/services/approvals.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, Any, Optional, Callable, Awaitable 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | ToolMetadataFetcher = Callable[[str], Awaitable[Optional[Dict[str, Any]]]] 7 | 8 | 9 | class ApprovalService: 10 | def __init__(self, tool_metadata_fetcher: Optional[ToolMetadataFetcher] = None): 11 | self.tool_metadata_fetcher = tool_metadata_fetcher 12 | 13 | async def close(self): 14 | return 15 | 16 | async def need_approval(self, tool: str, args: Dict[str, Any]) -> bool: 17 | try: 18 | if not self.tool_metadata_fetcher: 19 | return True 20 | 21 | tool_metadata = await self.tool_metadata_fetcher(tool) 22 | if not tool_metadata: 23 | return True 24 | 25 | annotations = tool_metadata.get("annotations", {}) 26 | read_only_hint = annotations.get("readOnlyHint", False) 27 | requires_approval = not read_only_hint 28 | 29 | return requires_approval 30 | 31 | except Exception: 32 | return True 33 | -------------------------------------------------------------------------------- /engine/.env.example: -------------------------------------------------------------------------------- 1 | # Application Settings 2 | APP_NAME=Skyflo.ai - Engine 3 | APP_VERSION=0.4.0 4 | APP_DESCRIPTION=Core engine for Skyflo.ai 5 | DEBUG=false 6 | API_V1_STR=/api/v1 7 | 8 | # Server Settings 9 | LOG_LEVEL=INFO 10 | 11 | # Database Settings (using postgres:// for Tortoise ORM) 12 | POSTGRES_DATABASE_URL=postgres://skyflo:skyflo@skyflo-ai-postgres:5432/skyflo 13 | 14 | # Redis Settings 15 | REDIS_URL=redis://skyflo-ai-redis:6379/0 16 | 17 | # MCP Server Settings 18 | MCP_SERVER_URL=http://skyflo-ai-mcp:8888/mcp 19 | 20 | # Rate Limiting 21 | RATE_LIMITING_ENABLED=true 22 | RATE_LIMIT_PER_MINUTE=100 23 | 24 | # JWT Settings 25 | JWT_SECRET=your-jwt-secret 26 | JWT_ALGORITHM=HS256 27 | JWT_ACCESS_TOKEN_EXPIRE_MINUTES=10080 # 1 week 28 | JWT_REFRESH_TOKEN_EXPIRE_DAYS=7 # 1 week 29 | 30 | # OpenAI Settings 31 | OPENAI_API_KEY=your-openai-api-key 32 | LLM_MODEL=openai/gpt-4o 33 | # GROQ_API_KEY= <> 34 | # LLM_MODEL=groq/meta-llama/llama-4-scout-17b-16e-instruct 35 | # LLM_MODEL=ollama/llama3.1:8b 36 | # LLM_HOST=0.0.0.0:11434 37 | LLM_TEMPERATURE=0.2 38 | LLM_MAX_ITERATIONS=25 39 | 40 | INTEGRATIONS_SECRET_NAMESPACE=skyflo-ai -------------------------------------------------------------------------------- /ui/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | dark: { 13 | DEFAULT: "#121214", 14 | secondary: "#1c1e24", 15 | navbar: "#070708", 16 | hover: "#16161a", 17 | active: "#1B1B1C", 18 | red: "#f54257", 19 | }, 20 | border: { 21 | DEFAULT: "#1c1c1c", 22 | focus: "#545457", 23 | menu: "#4E4E50", 24 | }, 25 | button: { 26 | primary: "#0F1D2F", // 2e87e6, purple: 8e30d1 27 | hover: "#1a6fc9", 28 | }, 29 | "primary-cyan": "#30CAF1", 30 | }, 31 | borderRadius: { 32 | lg: "var(--radius)", 33 | md: "calc(var(--radius) - 2px)", 34 | sm: "calc(var(--radius) - 4px)", 35 | }, 36 | fontFamily: { 37 | sans: ["Inter"], 38 | }, 39 | }, 40 | }, 41 | plugins: [], 42 | }; 43 | 44 | export default config; 45 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the changes and which issue is fixed. Explain the motivation for the changes. 4 | 5 | ## Related Issue(s) 6 | 7 | Fixes #(issue) 8 | 9 | ## Type of Change 10 | 11 | - [ ] Feature (new functionality) 12 | - [ ] Bug fix (fixes an issue) 13 | - [ ] Documentation update 14 | - [ ] Code refactor 15 | - [ ] Performance improvement 16 | - [ ] Tests 17 | - [ ] Infrastructure/build changes 18 | - [ ] Other (please describe): 19 | 20 | ## Testing 21 | 22 | Please describe the tests you've added/performed to verify your changes. 23 | 24 | ## Checklist 25 | 26 | - [ ] My code follows the [coding standards](CONTRIBUTING.md#coding-standards) for this project 27 | - [ ] I have added/updated necessary documentation 28 | - [ ] I have added tests that prove my fix/feature works 29 | - [ ] New and existing tests pass with my changes 30 | - [ ] I have checked for and resolved any merge conflicts 31 | - [ ] My commits follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) format 32 | - [ ] I have linked this PR to relevant issue(s) 33 | 34 | ## Screenshots (if applicable) 35 | 36 | ## Additional Notes -------------------------------------------------------------------------------- /ui/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function getCookie(name: string): string | null { 9 | if (typeof document === "undefined") return null; 10 | 11 | const cookies = document.cookie.split(";"); 12 | for (let i = 0; i < cookies.length; i++) { 13 | const cookie = cookies[i].trim(); 14 | if (cookie.startsWith(name + "=")) { 15 | return cookie.substring(name.length + 1); 16 | } 17 | } 18 | return null; 19 | } 20 | 21 | export function setCookie(name: string, value: string, days?: number): void { 22 | if (typeof document === "undefined") return; 23 | 24 | let expires = ""; 25 | if (days) { 26 | const date = new Date(); 27 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); 28 | expires = "; expires=" + date.toUTCString(); 29 | } 30 | 31 | document.cookie = name + "=" + value + expires + "; path=/"; 32 | } 33 | 34 | export function removeCookie(name: string): void { 35 | if (typeof document === "undefined") return; 36 | 37 | setCookie(name, "", -1); 38 | } 39 | -------------------------------------------------------------------------------- /deployment/ui/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | server_name localhost; 5 | 6 | location /api/v1/ { 7 | proxy_pass http://skyflo-ai-engine:8080/api/v1/; 8 | proxy_http_version 1.1; 9 | proxy_set_header Upgrade $http_upgrade; 10 | proxy_set_header Connection "Upgrade"; 11 | proxy_set_header Host $http_host; 12 | proxy_set_header X-Forwarded-Host $http_host; 13 | proxy_set_header X-Real-IP $remote_addr; 14 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 15 | proxy_set_header X-Forwarded-Proto $scheme; 16 | proxy_read_timeout 3600s; 17 | proxy_send_timeout 3600s; 18 | proxy_buffering off; 19 | chunked_transfer_encoding on; 20 | } 21 | 22 | location / { 23 | proxy_pass http://skyflo-ai-ui:3000; 24 | proxy_http_version 1.1; 25 | proxy_set_header Upgrade $http_upgrade; 26 | proxy_set_header Connection "Upgrade"; 27 | proxy_set_header Host $http_host; 28 | proxy_set_header X-Forwarded-Host $http_host; 29 | proxy_set_header X-Real-IP $remote_addr; 30 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 31 | proxy_set_header X-Forwarded-Proto $scheme; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /engine/src/api/middleware/logging_middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from typing import Callable 4 | 5 | from fastapi import Request, Response 6 | from starlette.middleware.base import BaseHTTPMiddleware 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class LoggingMiddleware(BaseHTTPMiddleware): 12 | 13 | async def dispatch(self, request: Request, call_next: Callable) -> Response: 14 | request_id = request.headers.get("X-Request-ID", "unknown") 15 | start_time = time.time() 16 | 17 | logger.debug(f"Request started [id={request_id}] {request.method} {request.url.path}") 18 | 19 | try: 20 | response = await call_next(request) 21 | 22 | process_time = time.time() - start_time 23 | 24 | logger.debug( 25 | f"Request completed [id={request_id}] {request.method} {request.url.path} " 26 | f"status={response.status_code} duration={process_time:.4f}s" 27 | ) 28 | 29 | response.headers["X-Process-Time"] = str(process_time) 30 | return response 31 | 32 | except Exception as e: 33 | logger.exception( 34 | f"Request failed [id={request_id}] {request.method} {request.url.path}: {str(e)}" 35 | ) 36 | raise 37 | -------------------------------------------------------------------------------- /mcp/tests/tools/test_helm.py: -------------------------------------------------------------------------------- 1 | """Tests for tools.helm module.""" 2 | 3 | import pytest 4 | from tools.helm import ( 5 | run_helm_command 6 | ) 7 | 8 | 9 | class TestRunHelmCommand: 10 | """Test cases for run_helm_command function.""" 11 | 12 | @pytest.mark.asyncio 13 | async def test_run_helm_command_basic(self, mocker): 14 | """Test basic helm command execution.""" 15 | mock_run_command = mocker.patch('tools.helm.run_command') 16 | mock_run_command.return_value = {"output": "helm output", "error": False} 17 | 18 | result = await run_helm_command("list") 19 | 20 | mock_run_command.assert_called_once_with("helm", ["list"]) 21 | assert result == {"output": "helm output", "error": False} 22 | 23 | @pytest.mark.asyncio 24 | async def test_run_helm_command_with_spaces(self, mocker): 25 | """Test helm command with multiple spaces.""" 26 | mock_run_command = mocker.patch('tools.helm.run_command') 27 | mock_run_command.return_value = {"output": "clean output", "error": False} 28 | 29 | result = await run_helm_command("install my-release nginx") 30 | 31 | mock_run_command.assert_called_once_with("helm", ["install", "my-release", "nginx"]) 32 | assert result == {"output": "clean output", "error": False} 33 | -------------------------------------------------------------------------------- /ui/src/store/useAuthStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist, createJSONStorage } from "zustand/middleware"; 3 | import { User, AuthState } from "@/types/auth"; 4 | 5 | interface AuthStore extends AuthState { 6 | login: (user: User, token: string) => void; 7 | logout: () => void; 8 | setLoading: (isLoading: boolean) => void; 9 | } 10 | 11 | export const useAuthStore = create()( 12 | persist( 13 | (set) => ({ 14 | user: null, 15 | token: null, 16 | isLoading: false, 17 | isAuthenticated: false, 18 | 19 | login: (user: User, token: string) => 20 | set({ 21 | user, 22 | token, 23 | isAuthenticated: true, 24 | isLoading: false, 25 | }), 26 | 27 | logout: () => 28 | set({ 29 | user: null, 30 | token: null, 31 | isAuthenticated: false, 32 | isLoading: false, 33 | }), 34 | 35 | setLoading: (isLoading: boolean) => set({ isLoading }), 36 | }), 37 | { 38 | name: "skyflo-auth-storage", 39 | storage: createJSONStorage(() => localStorage), 40 | partialize: (state) => ({ 41 | user: state.user, 42 | token: state.token, 43 | isAuthenticated: state.isAuthenticated, 44 | }), 45 | } 46 | ) 47 | ); 48 | -------------------------------------------------------------------------------- /mcp/tests/tools/test_argo.py: -------------------------------------------------------------------------------- 1 | """Tests for tools.argo module.""" 2 | 3 | import pytest 4 | from tools.argo import ( 5 | run_argo_command 6 | ) 7 | 8 | 9 | class TestRunArgoCommand: 10 | """Test cases for run_argo_command function.""" 11 | 12 | @pytest.mark.asyncio 13 | async def test_run_argo_command_basic(self, mocker): 14 | """Test basic argo command execution.""" 15 | mock_run_command = mocker.patch('tools.argo.run_command') 16 | mock_run_command.return_value = {"output": "argo output", "error": False} 17 | 18 | result = await run_argo_command("list rollouts") 19 | 20 | mock_run_command.assert_called_once_with("kubectl", ["argo", "rollouts", "list", "rollouts"]) 21 | assert result == {"output": "argo output", "error": False} 22 | 23 | @pytest.mark.asyncio 24 | async def test_run_argo_command_with_spaces(self, mocker): 25 | """Test argo command with multiple spaces.""" 26 | mock_run_command = mocker.patch('tools.argo.run_command') 27 | mock_run_command.return_value = {"output": "clean output", "error": False} 28 | 29 | result = await run_argo_command("get rollout my-rollout") 30 | 31 | mock_run_command.assert_called_once_with("kubectl", ["argo", "rollouts", "get", "rollout", "my-rollout"]) 32 | assert result == {"output": "clean output", "error": False} 33 | -------------------------------------------------------------------------------- /kubernetes-controller/engine/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Skyflo.ai. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the skyflo v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=skyflo.ai 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "skyflo.ai", Version: "v1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /ui/src/components/auth/AuthInput.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@/components/ui/input"; 2 | import { Label } from "@/components/ui/label"; 3 | import React from "react"; 4 | 5 | interface AuthInputProps { 6 | id: string; 7 | type: string; 8 | name: string; 9 | placeholder: string; 10 | icon: React.ComponentType<{ className?: string; size?: number }>; 11 | } 12 | 13 | export const AuthInput: React.FC = ({ 14 | id, 15 | type, 16 | name, 17 | placeholder, 18 | icon: Icon, 19 | }) => ( 20 |
21 | 24 |
25 |
26 | 27 |
28 | 36 |
37 |
38 | ); 39 | -------------------------------------------------------------------------------- /engine/src/api/services/limiter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | import redis.asyncio as redis 5 | from fastapi_limiter import FastAPILimiter 6 | 7 | from ..config import settings 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | _redis_client: Optional[redis.Redis] = None 12 | 13 | 14 | async def init_limiter() -> None: 15 | global _redis_client 16 | 17 | if not settings.RATE_LIMITING_ENABLED: 18 | logger.info("Rate limiting is disabled") 19 | return 20 | 21 | try: 22 | _redis_client = redis.from_url(settings.REDIS_URL, encoding="utf-8", decode_responses=True) 23 | 24 | await _redis_client.ping() 25 | 26 | await FastAPILimiter.init(_redis_client) 27 | 28 | logger.info(f"Rate limiter initialized with Redis at {settings.REDIS_URL}") 29 | except Exception as e: 30 | logger.error(f"Failed to initialize rate limiter: {str(e)}") 31 | logger.warning("Rate limiting will be disabled") 32 | 33 | 34 | async def close_limiter() -> None: 35 | global _redis_client 36 | 37 | if _redis_client is not None: 38 | await _redis_client.close() 39 | _redis_client = None 40 | logger.info("Rate limiter connection closed") 41 | 42 | 43 | async def get_redis_client() -> Optional[redis.Redis]: 44 | global _redis_client 45 | 46 | if _redis_client is None and settings.RATE_LIMITING_ENABLED: 47 | await init_limiter() 48 | 49 | return _redis_client 50 | -------------------------------------------------------------------------------- /ui/src/lib/debounce.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from "react"; 2 | 3 | export function debounce void>( 4 | func: T, 5 | delay: number 6 | ): T & { cancel: () => void } { 7 | let timeoutId: NodeJS.Timeout | null = null; 8 | 9 | const debouncedFunction = ((...args: Parameters) => { 10 | if (timeoutId) { 11 | clearTimeout(timeoutId); 12 | } 13 | 14 | timeoutId = setTimeout(() => { 15 | func(...args); 16 | }, delay); 17 | }) as T & { cancel: () => void }; 18 | 19 | debouncedFunction.cancel = () => { 20 | if (timeoutId) { 21 | clearTimeout(timeoutId); 22 | timeoutId = null; 23 | } 24 | }; 25 | 26 | return debouncedFunction; 27 | } 28 | 29 | export function useDebouncedFunction( 30 | func: (...args: T) => void, 31 | delay: number 32 | ) { 33 | const timeoutRef = useRef(null); 34 | 35 | const cancel = useCallback(() => { 36 | if (timeoutRef.current) { 37 | clearTimeout(timeoutRef.current); 38 | timeoutRef.current = null; 39 | } 40 | }, []); 41 | 42 | const execute = useCallback( 43 | (...args: T) => { 44 | cancel(); 45 | 46 | timeoutRef.current = setTimeout(() => { 47 | func(...args); 48 | }, delay); 49 | }, 50 | [func, delay, cancel] 51 | ); 52 | 53 | // Cleanup on unmount 54 | useEffect(() => { 55 | return cancel; 56 | }, [cancel]); 57 | 58 | return { execute, cancel }; 59 | } 60 | -------------------------------------------------------------------------------- /deployment/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export VERSION=v0.4.0 4 | 5 | print_colored() { 6 | local color=$1 7 | local message=$2 8 | if [ -t 1 ] && [ -n "$TERM" ] && [ "$TERM" != "dumb" ]; then 9 | case $color in 10 | "green") echo -e "\033[0;32m${message}\033[0m" ;; 11 | "red") echo -e "\033[0;31m${message}\033[0m" ;; 12 | "yellow") echo -e "\033[1;33m${message}\033[0m" ;; 13 | esac 14 | else 15 | echo "${message}" 16 | fi 17 | } 18 | 19 | print_colored "yellow" "Uninstalling Skyflo.ai..." 20 | 21 | 22 | # Namespace configuration 23 | read -p "Enter the name of your configured kubernetes namespace (press Enter for default 'skyflo-ai'): " NAMESPACE 24 | if [ -z "$NAMESPACE" ]; then 25 | NAMESPACE="skyflo-ai" 26 | print_colored "yellow" "ℹ processing the removal of the default namespace: skyflo-ai" 27 | else 28 | print_colored "yellow" "ℹ processing the removal of the configured namespace: $NAMESPACE" 29 | fi 30 | 31 | 32 | export NAMESPACE 33 | 34 | print_colored "yellow" "ℹ processing the removal of the skyflo.ai deployment" 35 | curl -sL "https://raw.githubusercontent.com/skyflo-ai/skyflo/main/deployment/install.yaml" | envsubst | kubectl delete -f - 36 | 37 | print_colored "yellow" "ℹ processing the removal of the skyflo.ai pvc" 38 | kubectl delete pvc -l app=skyflo-ai-postgres -n $NAMESPACE 39 | kubectl delete pvc -l app=skyflo-ai-redis -n $NAMESPACE 40 | 41 | print_colored "green" "ℹ skyflo.ai uninstalled successfully 42 | " -------------------------------------------------------------------------------- /engine/src/api/utils/helpers.py: -------------------------------------------------------------------------------- 1 | """Utility functions for the API.""" 2 | 3 | import logging 4 | from typing import Any, Optional 5 | from decouple import config, UndefinedValueError 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def get_state_value(state: Any, key: str, default: Any = None) -> Any: 12 | if hasattr(state, key): 13 | return getattr(state, key, default) 14 | elif hasattr(state, "get"): 15 | return state.get(key, default) 16 | else: 17 | return default 18 | 19 | 20 | def get_api_key_for_provider(provider: str) -> Optional[str]: 21 | """Get API key for a specific LLM provider from environment variables. 22 | 23 | Args: 24 | provider: Provider name (e.g., 'openai', 'groq'). Case-insensitive. 25 | 26 | Returns: 27 | API key for the provider if found, otherwise None. 28 | """ 29 | provider = provider.strip().upper() 30 | env_var_name = f"{provider}_API_KEY" 31 | try: 32 | api_key = config(env_var_name, default=None) 33 | if api_key: 34 | return api_key 35 | else: 36 | logger.warning(f"Environment variable {env_var_name} is set but empty.") 37 | return None 38 | except UndefinedValueError: 39 | logger.warning( 40 | f"API key environment variable {env_var_name} not found for provider '{provider}'." 41 | ) 42 | return None 43 | except Exception as e: 44 | logger.error(f"Error fetching API key for provider {provider}: {e}") 45 | return None 46 | -------------------------------------------------------------------------------- /engine/src/api/services/stop_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | import redis.asyncio as redis 5 | 6 | from ..config import settings 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | _redis_client: Optional[redis.Redis] = None 11 | 12 | 13 | async def _get_client() -> redis.Redis: 14 | global _redis_client 15 | if _redis_client is None: 16 | _redis_client = redis.from_url(settings.REDIS_URL, encoding="utf-8", decode_responses=True) 17 | return _redis_client 18 | 19 | 20 | def _stop_key(run_id: str) -> str: 21 | return f"agent:stop:{run_id}" 22 | 23 | 24 | async def request_stop(run_id: str, ttl_seconds: int = 600) -> None: 25 | try: 26 | client = await _get_client() 27 | await client.set(_stop_key(run_id), "1", ex=ttl_seconds) 28 | except Exception as e: 29 | logger.error(f"Failed to set stop flag for {run_id}: {e}") 30 | 31 | 32 | async def clear_stop(run_id: str) -> None: 33 | try: 34 | client = await _get_client() 35 | await client.delete(_stop_key(run_id)) 36 | except Exception as e: 37 | logger.error(f"Failed to clear stop flag for {run_id}: {e}") 38 | 39 | 40 | async def should_stop(run_id: Optional[str]) -> bool: 41 | if not run_id: 42 | return False 43 | try: 44 | client = await _get_client() 45 | value = await client.get(_stop_key(run_id)) 46 | return value == "1" 47 | except Exception as e: 48 | logger.error(f"Failed to read stop flag for {run_id}: {e}") 49 | return False 50 | -------------------------------------------------------------------------------- /engine/src/api/asgi.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from contextlib import asynccontextmanager 3 | from fastapi import FastAPI 4 | 5 | from .config import settings 6 | from .config import init_db, close_db_connection 7 | from .endpoints import api_router 8 | from .middleware import setup_middleware 9 | from .services.limiter import init_limiter, close_limiter 10 | from .services.checkpointer import init_graph_checkpointer, close_graph_checkpointer 11 | 12 | logging.basicConfig( 13 | level=getattr(logging, settings.LOG_LEVEL), 14 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 15 | ) 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | @asynccontextmanager 21 | async def lifespan(app: FastAPI): 22 | logger.info(f"Starting {settings.APP_NAME} version {settings.APP_VERSION}") 23 | await init_db() 24 | await init_limiter() 25 | await init_graph_checkpointer() 26 | 27 | yield 28 | 29 | logger.info(f"Shutting down {settings.APP_NAME}") 30 | await close_db_connection() 31 | await close_limiter() 32 | await close_graph_checkpointer() 33 | 34 | 35 | def create_application() -> FastAPI: 36 | application = FastAPI( 37 | title=settings.APP_NAME, 38 | description=settings.APP_DESCRIPTION, 39 | version=settings.APP_VERSION, 40 | debug=settings.DEBUG, 41 | lifespan=lifespan, 42 | ) 43 | 44 | setup_middleware(application) 45 | 46 | application.include_router(api_router, prefix=settings.API_V1_STR) 47 | 48 | return application 49 | 50 | 51 | app = create_application() 52 | -------------------------------------------------------------------------------- /ui/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const TooltipProvider = ({ 9 | delayDuration = 100, 10 | skipDelayDuration = 0, 11 | ...props 12 | }: React.ComponentProps) => ( 13 | 18 | ); 19 | 20 | const Tooltip = TooltipPrimitive.Root; 21 | 22 | const TooltipTrigger = TooltipPrimitive.Trigger; 23 | 24 | const TooltipContent = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, sideOffset = 4, ...props }, ref) => ( 28 | 29 | 38 | 39 | )); 40 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 41 | 42 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 43 | -------------------------------------------------------------------------------- /ui/src/app/api/integrations/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getAuthHeaders } from "@/lib/api"; 3 | 4 | export async function GET(request: NextRequest) { 5 | try { 6 | const headers = await getAuthHeaders(); 7 | const provider = request.nextUrl.searchParams.get("provider"); 8 | const url = new URL(`${process.env.API_URL}/integrations`); 9 | if (provider) url.searchParams.set("provider", provider); 10 | 11 | const resp = await fetch(url.toString(), { 12 | method: "GET", 13 | headers, 14 | cache: "no-store", 15 | }); 16 | 17 | const text = await resp.text(); 18 | const body = text ? JSON.parse(text) : {}; 19 | return NextResponse.json(body, { status: resp.status }); 20 | } catch (e) { 21 | return NextResponse.json( 22 | { status: "error", error: "Failed to fetch integrations" }, 23 | { status: 500 } 24 | ); 25 | } 26 | } 27 | 28 | export async function POST(request: NextRequest) { 29 | try { 30 | const headers = await getAuthHeaders(); 31 | const body = await request.json().catch(() => ({})); 32 | 33 | const resp = await fetch(`${process.env.API_URL}/integrations`, { 34 | method: "POST", 35 | headers: { ...headers, "Content-Type": "application/json" }, 36 | body: JSON.stringify(body || {}), 37 | }); 38 | 39 | const text = await resp.text(); 40 | const data = text ? JSON.parse(text) : {}; 41 | return NextResponse.json(data, { status: resp.status }); 42 | } catch (e) { 43 | return NextResponse.json( 44 | { status: "error", error: "Failed to create integration" }, 45 | { status: 500 } 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mcp/utils/commands.py: -------------------------------------------------------------------------------- 1 | """Shared command execution utilities for MCP tools.""" 2 | 3 | import asyncio 4 | from typing import Optional 5 | from .types import ToolOutput 6 | 7 | 8 | async def run_command( 9 | cmd: str, args: list[str], stdin: Optional[str] = None 10 | ) -> ToolOutput: 11 | """Run a command and return its output with error status.""" 12 | try: 13 | proc = await asyncio.create_subprocess_exec( 14 | cmd, 15 | *args, 16 | stdout=asyncio.subprocess.PIPE, 17 | stderr=asyncio.subprocess.PIPE, 18 | stdin=asyncio.subprocess.PIPE if stdin is not None else None, 19 | ) 20 | stdout, stderr = await proc.communicate( 21 | input=stdin.encode() if stdin is not None else None 22 | ) 23 | 24 | stdout_text = stdout.decode().strip() 25 | stderr_text = stderr.decode().strip() 26 | 27 | if proc.returncode != 0: 28 | return { 29 | "output": f"Error executing command {cmd} with args {args}: {stderr_text}", 30 | "error": True, 31 | } 32 | 33 | if stdout_text: 34 | output_text = stdout_text 35 | error = False 36 | elif stderr_text: 37 | output_text = stderr_text 38 | error = True 39 | else: 40 | output_text = ( 41 | "The command was executed successfully, but no output was returned." 42 | ) 43 | error = False 44 | 45 | return {"output": output_text, "error": error} 46 | except Exception as e: 47 | return { 48 | "output": f"Error executing command {cmd} with args {args}: {str(e)}", 49 | "error": True, 50 | } 51 | -------------------------------------------------------------------------------- /engine/src/api/models/integration.py: -------------------------------------------------------------------------------- 1 | """Integration model for external provider configs (e.g., Jenkins).""" 2 | 3 | import uuid 4 | from datetime import datetime 5 | from typing import Any, Dict, Optional 6 | 7 | from pydantic import BaseModel 8 | from tortoise import fields 9 | from tortoise.models import Model 10 | 11 | 12 | class Integration(Model): 13 | id = fields.UUIDField(pk=True, default=uuid.uuid4) 14 | 15 | user = fields.ForeignKeyField("models.User", related_name="integrations") 16 | 17 | provider = fields.CharField(max_length=50, unique=True) 18 | name = fields.CharField(max_length=100, null=True) 19 | 20 | metadata = fields.JSONField(null=True) 21 | 22 | credentials_ref = fields.CharField(max_length=255) 23 | 24 | status = fields.CharField(max_length=20, default="active") 25 | 26 | created_at = fields.DatetimeField(auto_now_add=True) 27 | updated_at = fields.DatetimeField(auto_now=True) 28 | 29 | class Meta: 30 | table = "integrations" 31 | unique_together = ("provider",) 32 | 33 | 34 | class IntegrationCreate(BaseModel): 35 | provider: str 36 | name: Optional[str] = None 37 | metadata: Optional[Dict[str, Any]] = None 38 | credentials: Dict[str, str] 39 | 40 | 41 | class IntegrationRead(BaseModel): 42 | id: uuid.UUID 43 | provider: str 44 | name: Optional[str] 45 | metadata: Optional[Dict[str, Any]] = None 46 | status: str 47 | created_at: datetime 48 | updated_at: datetime 49 | 50 | class Config: 51 | from_attributes = True 52 | 53 | 54 | class IntegrationUpdate(BaseModel): 55 | name: Optional[str] = None 56 | metadata: Optional[Dict[str, Any]] = None 57 | credentials: Optional[Dict[str, str]] = None 58 | status: Optional[str] = None 59 | -------------------------------------------------------------------------------- /engine/src/api/services/checkpointer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional, Any 3 | 4 | from langgraph.checkpoint.memory import MemorySaver 5 | from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver 6 | 7 | import psycopg 8 | from psycopg.rows import dict_row 9 | 10 | from ..config import settings 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | _checkpointer: Optional[Any] = None 16 | 17 | 18 | async def init_graph_checkpointer() -> None: 19 | global _checkpointer 20 | 21 | if _checkpointer is not None: 22 | return 23 | 24 | if not settings.ENABLE_POSTGRES_CHECKPOINTER: 25 | _checkpointer = MemorySaver() 26 | return 27 | 28 | try: 29 | conn = await psycopg.AsyncConnection.connect( 30 | settings.CHECKPOINTER_DATABASE_URL, 31 | autocommit=True, 32 | row_factory=dict_row, 33 | ) 34 | 35 | cp = AsyncPostgresSaver(conn) 36 | await cp.setup() 37 | _checkpointer = cp 38 | 39 | except Exception as e: 40 | logger.warning( 41 | f"Failed to initialize Postgres checkpointer: {e}. Falling back to in-memory." 42 | ) 43 | _checkpointer = MemorySaver() 44 | 45 | 46 | async def close_graph_checkpointer() -> None: 47 | global _checkpointer 48 | 49 | try: 50 | if _checkpointer is None: 51 | return 52 | 53 | if hasattr(_checkpointer, "aclose"): 54 | await _checkpointer.aclose() 55 | elif hasattr(_checkpointer, "conn") and hasattr(_checkpointer.conn, "aclose"): 56 | await _checkpointer.conn.aclose() 57 | finally: 58 | _checkpointer = None 59 | 60 | 61 | def get_checkpointer(): 62 | global _checkpointer 63 | return _checkpointer or MemorySaver() 64 | -------------------------------------------------------------------------------- /deployment/ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-slim AS builder 3 | 4 | RUN apt-get update && apt-get install -y \ 5 | bash \ 6 | curl \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | # Create app directory 10 | WORKDIR /app 11 | 12 | # Install dependencies first (caching) 13 | COPY ui/package.json ui/yarn.lock ./ 14 | RUN yarn install --frozen-lockfile 15 | 16 | # Copy source files and environment file 17 | COPY ui/tsconfig.json ./ 18 | COPY ui/next.config.mjs ./ 19 | COPY ui/tailwind.config.ts ./ 20 | COPY ui/postcss.config.mjs ./ 21 | COPY ui/components.json ./ 22 | COPY ui/.eslintrc.json ./ 23 | COPY ui/src ./src 24 | COPY ui/public ./public 25 | COPY ui/.env ./.env.production 26 | 27 | # Build the application 28 | ENV NEXT_TELEMETRY_DISABLED=1 29 | RUN yarn build 30 | 31 | # Production stage 32 | FROM node:20-slim AS runner 33 | 34 | WORKDIR /app 35 | 36 | # Create app user 37 | RUN groupadd -g 1002 skyflogroup \ 38 | && useradd -u 1002 -g skyflogroup -s /bin/bash -m skyflo \ 39 | && chown -R skyflo:skyflogroup /app 40 | 41 | ENV NODE_ENV=production 42 | ENV NEXT_TELEMETRY_DISABLED=1 43 | 44 | # Copy necessary files from builder 45 | COPY --from=builder --chown=skyflo:skyflogroup /app/.next/standalone ./ 46 | COPY --from=builder --chown=skyflo:skyflogroup /app/.next/static ./.next/static 47 | COPY --from=builder --chown=skyflo:skyflogroup /app/public ./public 48 | COPY --from=builder --chown=skyflo:skyflogroup /app/.env.production ./.env 49 | 50 | # Expose the UI port 51 | EXPOSE 3000 52 | 53 | LABEL org.opencontainers.image.source=https://github.com/skyflo-ai/skyflo 54 | LABEL org.opencontainers.image.description="UI for Skyflo.ai - Open Source AI Agent for Cloud & DevOps" 55 | 56 | # Switch to non-root user 57 | USER skyflo 58 | 59 | # Start the application 60 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /ui/src/app/api/profile/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { updateUserProfile, changePassword } from "@/lib/auth"; 3 | 4 | export async function PATCH(request: NextRequest) { 5 | try { 6 | const data = await request.json(); 7 | 8 | const result = await updateUserProfile(data); 9 | 10 | if (result.success) { 11 | return NextResponse.json(result.user, { status: 200 }); 12 | } else { 13 | return NextResponse.json({ error: result.error }, { status: 400 }); 14 | } 15 | } catch (error) { 16 | return NextResponse.json( 17 | { error: "Failed to update profile" }, 18 | { status: 500 } 19 | ); 20 | } 21 | } 22 | 23 | export async function POST(request: NextRequest) { 24 | try { 25 | const data = await request.json(); 26 | 27 | if (data.new_password && data.new_password.length < 8) { 28 | return NextResponse.json( 29 | { error: "Password must be at least 8 characters long" }, 30 | { status: 400 } 31 | ); 32 | } 33 | 34 | if (data.new_password !== data.confirm_password) { 35 | return NextResponse.json( 36 | { error: "Passwords do not match" }, 37 | { status: 400 } 38 | ); 39 | } 40 | 41 | const result = await changePassword({ 42 | current_password: data.current_password, 43 | new_password: data.new_password, 44 | }); 45 | 46 | if (result.success) { 47 | return NextResponse.json( 48 | { message: "Password updated" }, 49 | { status: 200 } 50 | ); 51 | } else { 52 | return NextResponse.json({ error: result.error }, { status: 400 }); 53 | } 54 | } catch (error) { 55 | return NextResponse.json( 56 | { error: "Failed to change password" }, 57 | { status: 500 } 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ui/src/components/ui/ToastContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ToastContainer as ReactToastifyContainer } from "react-toastify"; 3 | import "react-toastify/dist/ReactToastify.css"; 4 | 5 | export const ToastContainer: React.FC = () => { 6 | return ( 7 | <> 8 | 31 | 48 | 49 | ); 50 | }; 51 | 52 | export default ToastContainer; 53 | -------------------------------------------------------------------------------- /deployment/README.md: -------------------------------------------------------------------------------- 1 | # Skyflo Deployment 2 | 3 | ## Local Development with KinD 4 | 5 | ## Prerequisites 6 | 7 | - Docker 8 | - KinD 9 | - kubectl 10 | 11 | ## Setup KinD Cluster 12 | 13 | ```bash 14 | kind create cluster --name skyflo-ai --config deployment/local.kind.yaml 15 | ``` 16 | 17 | ## Build the Docker Images 18 | 19 | ```bash 20 | # Build the Engine image 21 | docker buildx build -f deployment/engine/Dockerfile -t skyfloaiagent/engine:latest . 22 | 23 | # Build the MCP image 24 | docker buildx build -f deployment/mcp/Dockerfile -t skyfloaiagent/mcp:latest . 25 | 26 | # Build the UI image 27 | docker buildx build -f deployment/ui/Dockerfile -t skyfloaiagent/ui:latest . 28 | 29 | # Build the Kubernetes Controller image 30 | docker buildx build -f deployment/kubernetes-controller/Dockerfile -t skyfloaiagent/controller:latest . 31 | 32 | # Build the Proxy image 33 | docker buildx build -f deployment/ui/proxy.Dockerfile -t skyfloaiagent/proxy:latest . 34 | ``` 35 | 36 | ## Load the built images into the KinD cluster 37 | ```bash 38 | kind load docker-image --name skyflo-ai skyfloaiagent/ui:latest 39 | 40 | kind load docker-image --name skyflo-ai skyfloaiagent/engine:latest 41 | 42 | kind load docker-image --name skyflo-ai skyfloaiagent/mcp:latest 43 | 44 | kind load docker-image --name skyflo-ai skyfloaiagent/controller:latest 45 | 46 | kind load docker-image --name skyflo-ai skyfloaiagent/proxy:latest 47 | ``` 48 | 49 | ## Install the Controller and Resources 50 | 51 | ```bash 52 | k delete -f local.install.yaml 53 | k apply -f local.install.yaml 54 | ``` 55 | 56 | ## How to test 57 | 58 | The Nginx deployment contains an incorrect image tag. This is a good basic test to see if the Sky AI agent catches the error and fixes it. 59 | 60 | ```bash 61 | k apply -f local.test-deploy.yaml 62 | 63 | k delete -f local.test-deploy.yaml 64 | ``` -------------------------------------------------------------------------------- /mcp/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # MCP module test runner 4 | # Usage: ./run_tests.sh [--coverage ] 5 | # Examples: 6 | # ./run_tests.sh 7 | # ./run_tests.sh --coverage 80 8 | 9 | set -euo pipefail 10 | 11 | # Default values 12 | COVERAGE_THRESHOLD=30 13 | 14 | # Parse arguments 15 | while [[ $# -gt 0 ]]; do 16 | case $1 in 17 | --coverage) 18 | COVERAGE_THRESHOLD="$2" 19 | shift 2 20 | ;; 21 | *) 22 | echo "❌ Unknown option: $1" 23 | echo "Usage: $0 [--coverage ]" 24 | exit 1 25 | ;; 26 | esac 27 | done 28 | 29 | echo "🐍 Setting up MCP test environment..." 30 | 31 | if [ -f ".venv/bin/activate" ]; then 32 | echo "🔑 Using existing .venv" 33 | source .venv/bin/activate 34 | 35 | if command -v uv >/dev/null 2>&1; then 36 | echo "🔄 Syncing dependencies with uv (including dev extras)" 37 | uv sync --extra dev 38 | else 39 | echo "⚠️ 'uv' not found. Installing dev deps with pip" 40 | python3 -m pip install -e ".[dev]" 41 | fi 42 | else 43 | echo "📦 Creating .venv and installing dependencies (per README)" 44 | python3 -m venv .venv 45 | source .venv/bin/activate 46 | 47 | if command -v uv >/dev/null 2>&1; then 48 | echo "📥 Installing with uv (including dev extras)" 49 | uv sync --extra dev 50 | else 51 | echo "📥 Installing with pip (uv not found)" 52 | python3 -m pip install -e . 53 | python3 -m pip install -e ".[dev]" 54 | fi 55 | fi 56 | 57 | # Run tests with coverage 58 | echo "🧪 Running tests with coverage (threshold: $COVERAGE_THRESHOLD%)..." 59 | python3 -m pytest tests/ --cov=. --cov-report=term --cov-fail-under="$COVERAGE_THRESHOLD" 60 | 61 | 62 | echo "✅ MCP tests completed successfully!" 63 | -------------------------------------------------------------------------------- /engine/src/api/config/database.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, Any 3 | 4 | from tortoise import Tortoise 5 | 6 | from . import settings 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | TORTOISE_ORM_CONFIG = { 11 | "connections": {"default": str(settings.POSTGRES_DATABASE_URL)}, 12 | "apps": { 13 | "models": { 14 | "models": [ 15 | "src.api.models.user", 16 | "src.api.models.conversation", 17 | "src.api.models.integration", 18 | "aerich.models", 19 | ], 20 | "default_connection": "default", 21 | } 22 | }, 23 | "use_tz": False, 24 | "timezone": "UTC", 25 | } 26 | 27 | 28 | async def init_db() -> None: 29 | try: 30 | logger.info("Initializing database connection") 31 | await Tortoise.init(config=TORTOISE_ORM_CONFIG) 32 | 33 | logger.info("Database connection established") 34 | except Exception as e: 35 | logger.exception(f"Failed to initialize database: {str(e)}") 36 | raise 37 | 38 | 39 | async def generate_schemas() -> None: 40 | try: 41 | logger.info("Generating database schemas") 42 | await Tortoise.generate_schemas() 43 | logger.info("Database schemas generated") 44 | except Exception as e: 45 | logger.exception(f"Failed to generate schemas: {str(e)}") 46 | raise 47 | 48 | 49 | async def close_db_connection() -> None: 50 | try: 51 | logger.info("Closing database connection") 52 | await Tortoise.close_connections() 53 | logger.info("Database connection closed") 54 | except Exception as e: 55 | logger.exception(f"Error closing database connection: {str(e)}") 56 | raise 57 | 58 | 59 | def get_tortoise_config() -> Dict[str, Any]: 60 | return TORTOISE_ORM_CONFIG 61 | -------------------------------------------------------------------------------- /ui/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )); 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )); 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 47 | 48 | export { ScrollArea, ScrollBar }; 49 | -------------------------------------------------------------------------------- /mcp/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "skyflo-mcp" 7 | dynamic = ["version"] 8 | description = 'Skyflo.ai MCP Server - Cloud Native Tools for AI Agents' 9 | readme = "README.md" 10 | requires-python = ">=3.11" 11 | license = "Apache-2.0" 12 | keywords = ["mcp", "ai", "cloud native", "kubernetes", "helm", "argo", "kubectl"] 13 | authors = [ 14 | { name = "Karan Jagtiani", email = "karan@skyflo.ai" }, 15 | ] 16 | 17 | dependencies = [ 18 | "mcp>=1.5.0", 19 | "pydantic>=2.10.6", 20 | "httpx>=0.28.1", 21 | "fastmcp>=2.12.3", 22 | ] 23 | 24 | [project.optional-dependencies] 25 | dev = [ 26 | "pytest>=8.0.2", 27 | "pytest-cov>=4.1.0", 28 | "pytest-asyncio>=0.23.5", 29 | "pytest-mock>=3.12.0", 30 | "mypy>=1.8.0", 31 | ] 32 | 33 | [project.scripts] 34 | skyflo-mcp = "main:main" 35 | 36 | [project.urls] 37 | Documentation = "https://github.com/skyflo-ai/skyflo#readme" 38 | Issues = "https://github.com/skyflo-ai/skyflo/issues" 39 | Source = "https://github.com/skyflo-ai/skyflo" 40 | 41 | [tool.hatch.version] 42 | path = "__about__.py" 43 | 44 | [tool.hatch.build.targets.wheel] 45 | packages = ["."] 46 | 47 | [tool.hatch.envs.default] 48 | dependencies = [ 49 | "pytest>=8.0.2", 50 | "pytest-cov>=4.1.0", 51 | "pytest-asyncio>=0.23.5", 52 | "mypy>=1.8.0", 53 | ] 54 | 55 | [tool.hatch.envs.default.scripts] 56 | test = "pytest {args:tests}" 57 | test-cov = "pytest --cov {args:.}" 58 | type-check = "mypy --install-types --non-interactive {args:. tests}" 59 | 60 | [tool.coverage.run] 61 | source = ["."] 62 | branch = true 63 | parallel = true 64 | omit = [ 65 | "__about__.py", 66 | "tests/*", 67 | ] 68 | 69 | [tool.coverage.paths] 70 | source = ["."] 71 | 72 | [tool.coverage.report] 73 | exclude_lines = [ 74 | "no cov", 75 | "if __name__ == .__main__.:", 76 | "if TYPE_CHECKING:", 77 | ] 78 | -------------------------------------------------------------------------------- /engine/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration files 8 | file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s 9 | 10 | # timezone to use when rendering the date 11 | # within the migration file as well as the filename. 12 | # string value is passed to dateutil.tz.gettz() 13 | # leave blank for localtime 14 | timezone = UTC 15 | 16 | # max length of characters to apply to the 17 | # "slug" field 18 | truncate_slug_length = 40 19 | 20 | # set to 'true' to run the environment during 21 | # the 'revision' command, regardless of autogenerate 22 | revision_environment = false 23 | 24 | # set to 'true' to allow .pyc and .pyo files without 25 | # a source .py file to be detected as revisions in the 26 | # versions/ directory 27 | # sourceless = false 28 | 29 | # version location specification; this defaults 30 | # to migrations/versions. When using multiple version 31 | # directories, initial revisions must be specified with --version-path 32 | # version_locations = %(here)s/bar %(here)s/bat migrations/versions 33 | 34 | # the output encoding used when revision files 35 | # are written from script.py.mako 36 | output_encoding = utf-8 37 | 38 | # Logging configuration 39 | [loggers] 40 | keys = root,sqlalchemy,alembic 41 | 42 | [handlers] 43 | keys = console 44 | 45 | [formatters] 46 | keys = generic 47 | 48 | [logger_root] 49 | level = WARN 50 | handlers = console 51 | qualname = 52 | 53 | [logger_sqlalchemy] 54 | level = WARN 55 | handlers = 56 | qualname = sqlalchemy.engine 57 | 58 | [logger_alembic] 59 | level = INFO 60 | handlers = 61 | qualname = alembic 62 | 63 | [handler_console] 64 | class = StreamHandler 65 | args = (sys.stderr,) 66 | level = NOTSET 67 | formatter = generic 68 | 69 | [formatter_generic] 70 | format = %(levelname)-5.5s [%(name)s] %(message)s 71 | datefmt = %H:%M:%S -------------------------------------------------------------------------------- /ui/src/components/ui/code-block.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { MdCheck, MdContentCopy } from "react-icons/md"; 3 | import { cn } from "@/lib/utils"; 4 | import { ScrollArea } from "@/components/ui/scroll-area"; 5 | 6 | interface CodeBlockProps { 7 | code: string; 8 | className?: string; 9 | } 10 | 11 | export function CodeBlock({ code, className }: CodeBlockProps) { 12 | const [copied, setCopied] = useState(false); 13 | 14 | const handleCopy = async () => { 15 | await navigator.clipboard.writeText(code); 16 | setCopied(true); 17 | setTimeout(() => setCopied(false), 2000); 18 | }; 19 | 20 | return ( 21 |
22 |
23 | 24 |
25 | 36 | 37 | 38 |
43 |             
44 |               {code}
45 |             
46 |           
47 |
48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /ui/src/app/api/integrations/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getAuthHeaders } from "@/lib/api"; 3 | 4 | export async function PATCH( 5 | request: NextRequest, 6 | { params }: { params: { id: string } } 7 | ) { 8 | try { 9 | const headers = await getAuthHeaders(); 10 | const body = await request.json().catch(() => ({})); 11 | 12 | const resp = await fetch( 13 | `${process.env.API_URL}/integrations/${params.id}`, 14 | { 15 | method: "PATCH", 16 | headers: { ...headers, "Content-Type": "application/json" }, 17 | body: JSON.stringify(body || {}), 18 | } 19 | ); 20 | 21 | const text = await resp.text(); 22 | const data = text ? JSON.parse(text) : {}; 23 | return NextResponse.json(data, { status: resp.status }); 24 | } catch (e) { 25 | return NextResponse.json( 26 | { status: "error", error: "Failed to update integration" }, 27 | { status: 500 } 28 | ); 29 | } 30 | } 31 | 32 | export async function DELETE( 33 | request: NextRequest, 34 | { params }: { params: { id: string } } 35 | ) { 36 | try { 37 | const headers = await getAuthHeaders(); 38 | const url = new URL(`${process.env.API_URL}/integrations/${params.id}`); 39 | 40 | const response = await fetch(url.toString(), { 41 | method: "DELETE", 42 | headers, 43 | }); 44 | 45 | if (!response.ok) { 46 | return NextResponse.json( 47 | { status: "error", error: "Failed to delete integration" }, 48 | { status: response.status } 49 | ); 50 | } 51 | 52 | if (response.status === 204) { 53 | return new NextResponse(null, { status: 204 }); 54 | } 55 | 56 | const text = await response.text(); 57 | const data = text ? JSON.parse(text) : {}; 58 | return NextResponse.json(data); 59 | } catch (e) { 60 | console.error(e); 61 | return NextResponse.json( 62 | { status: "error", error: "Failed to delete integration" }, 63 | { status: 500 } 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /engine/src/api/services/tools_cache.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Dict, List, Any, Callable, Awaitable, Optional 3 | 4 | 5 | class ToolsCache: 6 | def __init__(self) -> None: 7 | self._by_name: Dict[str, Dict[str, Any]] = {} 8 | self._all_dumped: List[Dict[str, Any]] = [] 9 | self._lock = asyncio.Lock() 10 | 11 | def invalidate(self) -> None: 12 | self._by_name.clear() 13 | self._all_dumped.clear() 14 | 15 | def _build(self, tools: List[Any]) -> None: 16 | by_name: Dict[str, Dict[str, Any]] = {} 17 | dumped: List[Dict[str, Any]] = [] 18 | for t in tools: 19 | d = t.model_dump() if hasattr(t, "model_dump") else t 20 | name = d.get("name") 21 | if isinstance(name, str) and name: 22 | by_name[name] = d 23 | dumped.append(d) 24 | self._by_name = by_name 25 | self._all_dumped = dumped 26 | 27 | async def _load(self, fetcher: Callable[[], Awaitable[List[Any]]]) -> None: 28 | tools = await fetcher() 29 | self._build(tools) 30 | 31 | async def ensure_loaded(self, fetcher: Callable[[], Awaitable[List[Any]]]) -> None: 32 | if self._all_dumped: 33 | return 34 | async with self._lock: 35 | if self._all_dumped: 36 | return 37 | await self._load(fetcher) 38 | 39 | async def get_all(self, fetcher: Callable[[], Awaitable[List[Any]]]) -> List[Dict[str, Any]]: 40 | await self.ensure_loaded(fetcher) 41 | return self._all_dumped 42 | 43 | async def get_by_name( 44 | self, name: str, fetcher: Callable[[], Awaitable[List[Any]]] 45 | ) -> Optional[Dict[str, Any]]: 46 | await self.ensure_loaded(fetcher) 47 | item = self._by_name.get(name) 48 | if item is not None: 49 | return item 50 | 51 | async with self._lock: 52 | if not self._all_dumped: 53 | await self._load(fetcher) 54 | return self._by_name.get(name) 55 | -------------------------------------------------------------------------------- /ui/src/app/api/conversation/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getAuthHeaders } from "@/lib/api"; 3 | 4 | export async function GET(request: NextRequest) { 5 | try { 6 | const headers = await getAuthHeaders(); 7 | 8 | const url = new URL(`${process.env.API_URL}/conversations`); 9 | const limit = request.nextUrl.searchParams.get("limit"); 10 | const cursor = request.nextUrl.searchParams.get("cursor"); 11 | const query = request.nextUrl.searchParams.get("query"); 12 | if (limit) url.searchParams.set("limit", limit); 13 | if (cursor) url.searchParams.set("cursor", cursor); 14 | if (query) url.searchParams.set("query", query); 15 | 16 | const response = await fetch(url.toString(), { 17 | method: "GET", 18 | headers, 19 | cache: "no-store", 20 | }); 21 | 22 | if (!response.ok) { 23 | return NextResponse.json( 24 | { status: "error", error: "Failed to fetch conversation history" }, 25 | { status: response.status } 26 | ); 27 | } 28 | 29 | const data = await response.json(); 30 | 31 | return NextResponse.json(data); 32 | } catch (error) { 33 | return NextResponse.json( 34 | { status: "error", error: "Failed to fetch conversation history" }, 35 | { status: 500 } 36 | ); 37 | } 38 | } 39 | 40 | export async function POST(request: Request) { 41 | try { 42 | const headers = await getAuthHeaders(); 43 | const body = await request.json().catch(() => ({})); 44 | 45 | const resp = await fetch(`${process.env.API_URL}/conversations`, { 46 | method: "POST", 47 | headers: { 48 | ...headers, 49 | "Content-Type": "application/json", 50 | }, 51 | body: JSON.stringify(body || {}), 52 | }); 53 | 54 | const data = await resp.json(); 55 | return NextResponse.json(data, { status: resp.status }); 56 | } catch (e) { 57 | return NextResponse.json( 58 | { status: "error", error: "Failed to create conversation" }, 59 | { status: 500 } 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ui/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stocksgenie-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@dagrejs/dagre": "^1.1.4", 13 | "@radix-ui/react-icons": "^1.3.0", 14 | "@radix-ui/react-label": "^2.1.0", 15 | "@radix-ui/react-scroll-area": "^1.2.0", 16 | "@radix-ui/react-slot": "^1.1.0", 17 | "@radix-ui/react-switch": "^1.1.1", 18 | "@radix-ui/react-tabs": "^1.1.0", 19 | "@radix-ui/react-tooltip": "^1.1.4", 20 | "@tsparticles/engine": "^3.0.2", 21 | "@tsparticles/react": "^3.0.0", 22 | "@tsparticles/slim": "^3.8.1", 23 | "@types/react-beautiful-dnd": "^13.1.8", 24 | "@xyflow/react": "^12.3.2", 25 | "aws-icons": "^2.1.0", 26 | "canvas-confetti": "^1.9.3", 27 | "class-variance-authority": "^0.7.0", 28 | "clsx": "^2.1.1", 29 | "date-fns": "^4.1.0", 30 | "framer-motion": "^12.6.2", 31 | "html-to-image": "^1.11.11", 32 | "http-proxy-middleware": "^3.0.5", 33 | "newrelic": "^12.8.0", 34 | "next": "14.2.11", 35 | "react": "^18", 36 | "react-beautiful-dnd": "^13.1.1", 37 | "react-code-blocks": "^0.1.6", 38 | "react-dnd": "^16.0.1", 39 | "react-dnd-html5-backend": "^16.0.1", 40 | "react-dom": "^18", 41 | "react-icons": "^5.3.0", 42 | "react-markdown": "^9.0.1", 43 | "react-svg": "^16.1.34", 44 | "react-toastify": "^11.0.5", 45 | "react-tsparticles": "^2.12.2", 46 | "recharts": "^2.12.7", 47 | "remark-breaks": "^4.0.0", 48 | "remark-gfm": "^4.0.0", 49 | "sharp": "^0.33.5", 50 | "socket.io-client": "^4.8.1", 51 | "tailwind-merge": "^3.2.0", 52 | "tailwindcss-animate": "^1.0.7", 53 | "tsparticles": "^3.8.1" 54 | }, 55 | "devDependencies": { 56 | "@types/node": "^20", 57 | "@types/react": "^18", 58 | "@types/react-dom": "^18", 59 | "eslint": "^8", 60 | "eslint-config-next": "14.2.11", 61 | "postcss": "^8", 62 | "tailwindcss": "^3.4.1", 63 | "typescript": "^5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /engine/migrations/models/0_20250903153104_init.py: -------------------------------------------------------------------------------- 1 | from tortoise import BaseDBAsyncClient 2 | 3 | 4 | async def upgrade(db: BaseDBAsyncClient) -> str: 5 | return """ 6 | CREATE TABLE IF NOT EXISTS "users" ( 7 | "id" UUID NOT NULL PRIMARY KEY, 8 | "email" VARCHAR(255) NOT NULL UNIQUE, 9 | "hashed_password" VARCHAR(255) NOT NULL, 10 | "full_name" VARCHAR(255), 11 | "is_active" BOOL NOT NULL DEFAULT True, 12 | "is_superuser" BOOL NOT NULL DEFAULT False, 13 | "is_verified" BOOL NOT NULL DEFAULT False, 14 | "role" VARCHAR(20) NOT NULL DEFAULT 'member', 15 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP 17 | ); 18 | CREATE INDEX IF NOT EXISTS "idx_users_email_133a6f" ON "users" ("email"); 19 | COMMENT ON TABLE "users" IS 'User model for authentication and profile information.'; 20 | CREATE TABLE IF NOT EXISTS "conversations" ( 21 | "id" UUID NOT NULL PRIMARY KEY, 22 | "title" VARCHAR(255), 23 | "is_active" BOOL NOT NULL DEFAULT True, 24 | "conversation_metadata" JSONB, 25 | "messages_json" JSONB, 26 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 27 | "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 28 | "user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE 29 | ); 30 | COMMENT ON TABLE "conversations" IS 'Conversation model for tracking chat sessions.'; 31 | CREATE TABLE IF NOT EXISTS "messages" ( 32 | "id" UUID NOT NULL PRIMARY KEY, 33 | "role" VARCHAR(50) NOT NULL, 34 | "content" TEXT NOT NULL, 35 | "sequence" INT NOT NULL, 36 | "message_metadata" JSONB, 37 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 38 | "conversation_id" UUID NOT NULL REFERENCES "conversations" ("id") ON DELETE CASCADE 39 | ); 40 | COMMENT ON TABLE "messages" IS 'Message model for storing individual chat messages.'; 41 | CREATE TABLE IF NOT EXISTS "aerich" ( 42 | "id" SERIAL NOT NULL PRIMARY KEY, 43 | "version" VARCHAR(255) NOT NULL, 44 | "app" VARCHAR(100) NOT NULL, 45 | "content" JSONB NOT NULL 46 | );""" 47 | 48 | 49 | async def downgrade(db: BaseDBAsyncClient) -> str: 50 | return """ 51 | """ 52 | -------------------------------------------------------------------------------- /engine/src/api/models/user.py: -------------------------------------------------------------------------------- 1 | """User model definition.""" 2 | 3 | import uuid 4 | from typing import Optional 5 | from datetime import datetime 6 | 7 | from fastapi_users.schemas import BaseUser, BaseUserCreate, BaseUserUpdate 8 | from tortoise import fields, models 9 | 10 | 11 | class User(models.Model): 12 | """User model for authentication and profile information.""" 13 | 14 | id = fields.UUIDField(pk=True, default=uuid.uuid4) 15 | email = fields.CharField(max_length=255, unique=True, index=True) 16 | hashed_password = fields.CharField(max_length=255) 17 | full_name = fields.CharField(max_length=255, null=True) 18 | is_active = fields.BooleanField(default=True) 19 | is_superuser = fields.BooleanField(default=False) 20 | is_verified = fields.BooleanField(default=False) 21 | role = fields.CharField(max_length=20, default="member") 22 | created_at = fields.DatetimeField(auto_now_add=True) 23 | updated_at = fields.DatetimeField(auto_now=True) 24 | 25 | conversations = fields.ReverseRelation["Conversation"] 26 | 27 | class Meta: 28 | """Tortoise ORM model configuration.""" 29 | 30 | table = "users" 31 | 32 | def __str__(self) -> str: 33 | """String representation of the user.""" 34 | return f"" 35 | 36 | 37 | class UserCreate(BaseUserCreate): 38 | """Schema for user creation.""" 39 | 40 | full_name: Optional[str] = None 41 | role: Optional[str] = "member" 42 | 43 | 44 | class UserRead(BaseUser[uuid.UUID]): 45 | """Schema for reading user data.""" 46 | 47 | full_name: Optional[str] = None 48 | role: str 49 | created_at: datetime 50 | 51 | class Config: 52 | from_attributes = True 53 | 54 | 55 | class UserUpdate(BaseUserUpdate): 56 | """Schema for updating user data.""" 57 | 58 | full_name: Optional[str] = None 59 | role: Optional[str] = None 60 | 61 | 62 | class UserDB(BaseUser[uuid.UUID]): 63 | """Schema for user in database with hashed password.""" 64 | 65 | hashed_password: str 66 | full_name: Optional[str] = None 67 | role: str = "member" 68 | created_at: datetime 69 | updated_at: datetime 70 | 71 | class Config: 72 | from_attributes = True 73 | -------------------------------------------------------------------------------- /ui/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )); 42 | CardTitle.displayName = "CardTitle"; 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )); 54 | CardDescription.displayName = "CardDescription"; 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )); 62 | CardContent.displayName = "CardContent"; 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )); 74 | CardFooter.displayName = "CardFooter"; 75 | 76 | export { 77 | Card, 78 | CardHeader, 79 | CardFooter, 80 | CardTitle, 81 | CardDescription, 82 | CardContent, 83 | }; 84 | -------------------------------------------------------------------------------- /deployment/engine/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use official Python image 2 | FROM python:3.11-slim 3 | 4 | ARG TARGETARCH 5 | 6 | # Install system dependencies 7 | RUN apt-get update && apt-get install -y --no-install-recommends \ 8 | git \ 9 | build-essential \ 10 | curl \ 11 | ca-certificates \ 12 | netcat-openbsd \ 13 | bash \ 14 | postgresql-client \ 15 | && apt-get clean \ 16 | && rm -rf /var/lib/apt/lists/* 17 | 18 | # Create app user and directory 19 | RUN groupadd -g 1002 skyflogroup \ 20 | && useradd -u 1002 -g skyflogroup -s /bin/bash -m skyflo \ 21 | && mkdir -p /app \ 22 | && chown -R skyflo:skyflogroup /app 23 | 24 | # Set up application 25 | WORKDIR /app 26 | 27 | # Create and activate virtual environment early 28 | ENV VIRTUAL_ENV="/app/venv" 29 | RUN python -m venv $VIRTUAL_ENV 30 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 31 | 32 | # Create the source directory structure first 33 | RUN mkdir -p src/api 34 | 35 | # Copy dependency-related files and source code needed for installation 36 | COPY engine/pyproject.toml engine/.python-version engine/uv.lock engine/README.md ./ 37 | COPY engine/src/api/__about__.py ./src/api/ 38 | 39 | # Install dependencies 40 | RUN pip install --upgrade pip && \ 41 | pip install -e . && \ 42 | pip install uvicorn[standard] 43 | 44 | # Now copy all remaining application files 45 | COPY engine/src ./src 46 | COPY engine/.env ./.env 47 | COPY engine/migrations ./migrations 48 | COPY engine/aerich.ini ./aerich.ini 49 | COPY engine/alembic.ini ./alembic.ini 50 | 51 | # Copy and set up entrypoint script 52 | COPY deployment/engine/entrypoint.sh /app/entrypoint.sh 53 | RUN chmod +x /app/entrypoint.sh 54 | 55 | # Set final permissions 56 | RUN chown -R skyflo:skyflogroup /app && \ 57 | chmod -R 755 /app 58 | 59 | # Update PATH and PYTHONPATH for skyflo user 60 | ENV PATH="/app/venv/bin:/usr/local/bin:/home/skyflo/.local/bin:${PATH}" \ 61 | PYTHONPATH="/app/src" 62 | 63 | # Expose the API port 64 | EXPOSE 8080 65 | 66 | LABEL org.opencontainers.image.source=https://github.com/skyflo-ai/skyflo 67 | LABEL org.opencontainers.image.description="Skyflo.ai Engine Service - Open Source AI Agent for Cloud & DevOps" 68 | 69 | # Switch to non-root user 70 | USER skyflo 71 | 72 | # Use the entrypoint script 73 | ENTRYPOINT ["/app/entrypoint.sh"] -------------------------------------------------------------------------------- /ui/src/lib/api.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | export async function getAuthHeaders() { 6 | try { 7 | const nextCookies = cookies().getAll(); 8 | const cookieHeader = nextCookies 9 | .map((cookie) => `${cookie.name}=${cookie.value}`) 10 | .join("; "); 11 | 12 | const tokenCookie = 13 | nextCookies.find((cookie) => cookie.name === "auth_token") || 14 | nextCookies.find((cookie) => cookie.name === "access_token") || 15 | nextCookies.find((cookie) => cookie.name === "token"); 16 | 17 | const headers: HeadersInit = { 18 | "Content-Type": "application/json", 19 | Cookie: cookieHeader, 20 | }; 21 | 22 | if (tokenCookie) { 23 | headers["Authorization"] = `Bearer ${tokenCookie.value}`; 24 | } 25 | 26 | return headers; 27 | } catch (error) { 28 | return { "Content-Type": "application/json" }; 29 | } 30 | } 31 | 32 | export const createConversation = async ( 33 | clientConversationId?: string 34 | ): Promise => { 35 | try { 36 | const response = await fetch(`${process.env.API_URL}/conversations`, { 37 | method: "POST", 38 | headers: { 39 | ...(await getAuthHeaders()), 40 | "Content-Type": "application/json", 41 | }, 42 | body: JSON.stringify({ 43 | conversation_id: clientConversationId, 44 | title: `New Conversation ${new Date().toISOString().split("T")[0]}`, 45 | }), 46 | }); 47 | 48 | if (!response.ok) { 49 | throw new Error(`Error creating conversation: ${response.statusText}`); 50 | } 51 | 52 | const data = await response.json(); 53 | 54 | const maxRetries = 3; 55 | const baseDelay = 100; // 100ms initial delay 56 | 57 | for (let attempt = 0; attempt < maxRetries; attempt++) { 58 | const checkResponse = await fetch( 59 | `${process.env.API_URL}/conversations/${data.id}`, 60 | { 61 | headers: await getAuthHeaders(), 62 | } 63 | ); 64 | 65 | if (checkResponse.ok) { 66 | return data; 67 | } 68 | 69 | if (attempt < maxRetries - 1) { 70 | const delay = baseDelay * Math.pow(2, attempt); 71 | await new Promise((resolve) => setTimeout(resolve, delay)); 72 | continue; 73 | } 74 | } 75 | 76 | return data; 77 | } catch (error) { 78 | throw error; 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /kubernetes-controller/cmd/manager/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 8 | _ "k8s.io/client-go/plugin/pkg/client/auth" 9 | 10 | "k8s.io/apimachinery/pkg/runtime" 11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 12 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 13 | ctrl "sigs.k8s.io/controller-runtime" 14 | "sigs.k8s.io/controller-runtime/pkg/healthz" 15 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 16 | ) 17 | 18 | var ( 19 | scheme = runtime.NewScheme() 20 | setupLog = ctrl.Log.WithName("setup") 21 | ) 22 | 23 | func init() { 24 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 25 | // +kubebuilder:scaffold:scheme 26 | } 27 | 28 | func main() { 29 | var metricsAddr string 30 | var enableLeaderElection bool 31 | var probeAddr string 32 | 33 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 34 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 35 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 36 | "Enable leader election for controller manager. "+ 37 | "Enabling this will ensure there is only one active controller manager.") 38 | 39 | opts := zap.Options{ 40 | Development: true, 41 | } 42 | opts.BindFlags(flag.CommandLine) 43 | flag.Parse() 44 | 45 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 46 | 47 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 48 | Scheme: scheme, 49 | HealthProbeBindAddress: probeAddr, 50 | LeaderElection: enableLeaderElection, 51 | LeaderElectionID: "skyflo-controller.skyflo.ai", 52 | WebhookServer: nil, // We'll configure webhooks later when needed 53 | }) 54 | if err != nil { 55 | setupLog.Error(err, "unable to start manager") 56 | os.Exit(1) 57 | } 58 | 59 | // TODO: Register controllers here once we create them 60 | 61 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 62 | setupLog.Error(err, "unable to set up health check") 63 | os.Exit(1) 64 | } 65 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 66 | setupLog.Error(err, "unable to set up ready check") 67 | os.Exit(1) 68 | } 69 | 70 | setupLog.Info("starting manager") 71 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 72 | setupLog.Error(err, "problem running manager") 73 | os.Exit(1) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ui/src/components/auth/Login.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import { Button } from "@/components/ui/button"; 6 | import { AuthInput } from "./AuthInput"; 7 | import { MdLock, MdEmail } from "react-icons/md"; 8 | import { useAuthStore } from "@/store/useAuthStore"; 9 | import { handleLogin } from "@/lib/auth"; 10 | import { setCookie } from "@/lib/utils"; 11 | import { showError } from "../ui/toast"; 12 | 13 | export const Login = () => { 14 | const { login } = useAuthStore(); 15 | const [loading, setLoading] = useState(false); 16 | const [isMounted, setIsMounted] = useState(false); 17 | const router = useRouter(); 18 | 19 | useEffect(() => { 20 | setIsMounted(true); 21 | }, []); 22 | 23 | const handleSubmit = async (e: React.FormEvent) => { 24 | e.preventDefault(); 25 | if (!isMounted) return; 26 | 27 | setLoading(true); 28 | 29 | const formData = new FormData(e.currentTarget); 30 | const result = await handleLogin(formData); 31 | 32 | if (result && result.success) { 33 | setCookie("auth_token", result.token, 7); 34 | login(result.user, result.token); 35 | router.push("/"); 36 | } else { 37 | showError(result?.error || "Authentication failed"); 38 | } 39 | 40 | setLoading(false); 41 | }; 42 | 43 | if (!isMounted) { 44 | return null; 45 | } 46 | 47 | return ( 48 |
49 | 56 | 63 | 77 | 78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /ui/src/components/chat/QueuedMessagesBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MdArrowUpward, MdDelete } from "react-icons/md"; 4 | 5 | type QueuedItem = { id: string; content: string; timestamp: number }; 6 | 7 | interface QueuedMessagesBarProps { 8 | items: QueuedItem[]; 9 | onSubmitNow: (id: string) => void; 10 | onRemove: (id: string) => void; 11 | } 12 | 13 | export function QueuedMessagesBar({ 14 | items, 15 | onSubmitNow, 16 | onRemove, 17 | }: QueuedMessagesBarProps) { 18 | if (!items || items.length === 0) return null; 19 | 20 | return ( 21 |
22 |
23 | {items.length} Queued 24 |
25 |
26 | {items.map((m) => ( 27 |
32 |
33 | 34 | {m.content} 35 | 36 |
37 | 47 | 57 |
58 |
59 |
60 | ))} 61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /ui/src/types/chat.ts: -------------------------------------------------------------------------------- 1 | export interface ToolExecution { 2 | call_id: string; 3 | tool: string; 4 | title: string; 5 | args: Record; 6 | status: 7 | | "executing" 8 | | "completed" 9 | | "error" 10 | | "pending" 11 | | "awaiting_approval" 12 | | "denied" 13 | | "approved"; 14 | result?: Array<{ 15 | type: string; 16 | text?: string; 17 | annotations?: any; 18 | }>; 19 | timestamp: number; 20 | error?: string; 21 | requires_approval?: boolean; 22 | } 23 | 24 | export type MessageSegment = 25 | | { 26 | kind: "text"; 27 | id: string; 28 | text: string; 29 | timestamp?: number; 30 | } 31 | | { 32 | kind: "tool"; 33 | id: string; 34 | toolExecution: ToolExecution; 35 | timestamp?: number; 36 | }; 37 | 38 | export interface ChatMessage { 39 | id: string; 40 | type: "user" | "assistant"; 41 | content: string; 42 | timestamp: number; 43 | isStreaming?: boolean; 44 | segments?: MessageSegment[]; 45 | tokenUsage?: TokenUsage; 46 | } 47 | 48 | export interface ChatState { 49 | messages: ChatMessage[]; 50 | currentMessage: ChatMessage | null; 51 | toolExecutions: ToolExecution[]; 52 | isStreaming: boolean; 53 | isConnected: boolean; 54 | error: string | null; 55 | currentRunId: string | null; 56 | } 57 | 58 | export interface ChatInterfaceProps { 59 | conversationId: string; 60 | initialMessage?: string; 61 | } 62 | 63 | export interface ChatMessagesProps { 64 | messages: ChatMessage[]; 65 | currentMessage?: ChatMessage | null; 66 | toolExecutions: ToolExecution[]; 67 | isStreaming: boolean; 68 | } 69 | 70 | export interface ToolVisualizationProps { 71 | toolExecution: ToolExecution; 72 | isExpanded?: boolean; 73 | onToggleExpand?: () => void; 74 | } 75 | 76 | export interface ChatInputProps { 77 | inputValue: string; 78 | setInputValue: (value: string) => void; 79 | handleSubmit: (e?: React.FormEvent) => void; 80 | handleNewChat: () => void; 81 | isStreaming: boolean; 82 | hasMessages?: boolean; 83 | onCancel?: () => void; 84 | tokenUsage?: TokenUsage; 85 | } 86 | 87 | export interface Suggestion { 88 | text: string; 89 | category: string; 90 | icon: React.ComponentType; 91 | } 92 | 93 | export interface TokenUsage { 94 | prompt_tokens: number; 95 | completion_tokens: number; 96 | total_tokens: number; 97 | cached_tokens: number; 98 | ttft?: number; 99 | ttr?: number; 100 | } 101 | -------------------------------------------------------------------------------- /engine/src/api/config/settings.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from pydantic import Field 3 | from pydantic_settings import BaseSettings 4 | 5 | 6 | class Settings(BaseSettings): 7 | 8 | APP_NAME: str 9 | APP_VERSION: str 10 | APP_DESCRIPTION: str 11 | DEBUG: bool = False 12 | API_V1_STR: str = "/api/v1" 13 | 14 | LOG_LEVEL: str = "INFO" 15 | 16 | POSTGRES_DATABASE_URL: str = Field(default="postgres://postgres:postgres@localhost:5432/skyflo") 17 | 18 | CHECKPOINTER_DATABASE_URL: Optional[str] = Field(default=None) 19 | ENABLE_POSTGRES_CHECKPOINTER: bool = Field(default=True) 20 | 21 | REDIS_URL: str = "redis://localhost:6379/0" 22 | 23 | RATE_LIMITING_ENABLED: bool = True 24 | RATE_LIMIT_PER_MINUTE: int = 100 25 | 26 | JWT_SECRET: str = "CHANGE_ME_IN_PRODUCTION" 27 | JWT_ALGORITHM: str = "HS256" 28 | JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 10080 # One week in minutes 29 | JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 7 30 | 31 | MCP_SERVER_URL: str = "http://127.0.0.1:8888/mcp" 32 | 33 | INTEGRATIONS_SECRET_NAMESPACE: Optional[str] = Field(default="default") 34 | 35 | MAX_AUTO_CONTINUE_TURNS: int = 2 36 | 37 | LLM_MODEL: Optional[str] = Field(default="openai/gpt-4o", env="LLM_MODEL") 38 | LLM_HOST: Optional[str] = Field(default=None, env="LLM_HOST") 39 | OPENAI_API_KEY: Optional[str] = Field(default=None, env="OPENAI_API_KEY") 40 | LLM_MAX_ITERATIONS: int = 25 41 | 42 | LLM_TEMPERATURE: float = 0.2 43 | MODEL_NAME: str = "gpt-4o" 44 | AGENT_TYPE: str = "assistant" 45 | TEMPERATURE: float = 0.2 46 | 47 | class Config: 48 | env_file = ".env" 49 | env_file_encoding = "utf-8" 50 | case_sensitive = True 51 | extra = "ignore" 52 | 53 | def __init__(self, **kwargs): 54 | super().__init__(**kwargs) 55 | 56 | if self.POSTGRES_DATABASE_URL and "postgresql+" in self.POSTGRES_DATABASE_URL: 57 | self.POSTGRES_DATABASE_URL = self.POSTGRES_DATABASE_URL.replace( 58 | "postgresql+psycopg://", "postgres://" 59 | ) 60 | 61 | if not self.CHECKPOINTER_DATABASE_URL: 62 | self.CHECKPOINTER_DATABASE_URL = self._get_checkpointer_url() 63 | 64 | def _get_checkpointer_url(self) -> str: 65 | url = self.POSTGRES_DATABASE_URL 66 | 67 | if "sslmode=" not in url: 68 | separator = "&" if "?" in url else "?" 69 | url = f"{url}{separator}sslmode=disable" 70 | 71 | return url 72 | 73 | 74 | settings = Settings() 75 | 76 | 77 | def get_settings() -> Settings: 78 | return settings 79 | -------------------------------------------------------------------------------- /ui/src/components/chat/ChatSuggestions.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { MdSearch, MdElectricBolt, MdRefresh, MdDelete } from "react-icons/md"; 3 | 4 | const INITIAL_SUGGESTIONS = [ 5 | { 6 | text: "Get all pods in the ... namespace", 7 | icon: MdSearch, 8 | category: "Query", 9 | }, 10 | { 11 | text: "Create a new deployment in the ... namespace", 12 | icon: MdElectricBolt, 13 | category: "Create Deployment", 14 | }, 15 | { 16 | text: "Restart all deployments in the ... namespace", 17 | icon: MdRefresh, 18 | category: "Restart Deployment", 19 | }, 20 | { 21 | text: "Delete the ... service in the ... namespace", 22 | icon: MdDelete, 23 | category: "Delete Resource", 24 | }, 25 | ]; 26 | 27 | export interface ChatSuggestionsProps { 28 | onSuggestionClick: (suggestion: string) => void; 29 | } 30 | 31 | const cardVariants = { 32 | hidden: { opacity: 0, y: 10 }, 33 | visible: (index: number) => ({ 34 | opacity: 1, 35 | y: 0, 36 | transition: { 37 | delay: index * 0.05, 38 | duration: 0.3, 39 | type: "spring", 40 | stiffness: 120, 41 | damping: 25, 42 | }, 43 | }), 44 | }; 45 | 46 | const ChatSuggestions = ({ onSuggestionClick }: ChatSuggestionsProps) => { 47 | return ( 48 |
49 | {INITIAL_SUGGESTIONS.map((suggestion, index) => ( 50 | onSuggestionClick(suggestion.text)} 63 | aria-label={`Select suggestion: ${suggestion.text}`} 64 | > 65 | 66 | 67 | 68 | 69 | {suggestion.text} 70 | 71 | 72 | ))} 73 |
74 | ); 75 | }; 76 | 77 | export default ChatSuggestions; 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Skyflo.ai 2 | 3 | Thank you for considering contributing to Skyflo.ai! This document provides guidelines for contributing to the project. 4 | 5 | We are committed to providing a friendly, safe, and welcoming environment for all contributors. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md). 6 | 7 | We highly recommend reading our [Architecture Guide](docs/architecture.md) if you'd like to contribute! The repo is not as intimidating as it first seems if you read the guide! 8 | 9 | ## Quick Start 10 | 11 | 1. **Find an Issue**: Browse [issues](https://github.com/skyflo-ai/skyflo/issues) or [create one](https://github.com/skyflo-ai/skyflo/issues/new/choose) 12 | 2. **Fork & Clone**: Fork the repository and clone it locally 13 | 3. **Setup**: Install dependencies and configure development environment 14 | 4. **Create Branch**: Use `feature/issue-number-description` or `fix/issue-number-description` 15 | 5. **Make Changes**: Follow our coding standards and add tests 16 | 6. **Submit PR**: Create a pull request with a clear description of changes 17 | 18 | ## Coding Standards 19 | 20 | - **Python**: [PEP 8](https://www.python.org/dev/peps/pep-0008/), type hints, docstrings 21 | - **JavaScript/TypeScript**: [Airbnb Style Guide](https://github.com/airbnb/javascript), TypeScript for type safety 22 | - **Go**: [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) 23 | - **Documentation**: Markdown, clear language, code examples 24 | - **Commits**: [Conventional Commits](https://www.conventionalcommits.org/) format 25 | - Include component scope in commit messages: `type(scope): message` 26 | - Use the following component scopes: 27 | - `ui`: Frontend components 28 | - `engine`: Engine components 29 | - `mcp`: MCP server components 30 | - `k8s`: Kubernetes controller components 31 | - `docs`: Documentation changes 32 | - `infra`: Infrastructure or build system changes 33 | - Example: `feat (mcp): add docker tools` or `fix (ui): resolve workflow visualizer overflow` 34 | 35 | ## Pull Request Process 36 | 37 | 1. Fill out the PR template 38 | 2. Link to related issues 39 | 3. Ensure CI checks pass 40 | 4. Address review feedback 41 | 5. Await approval from maintainer 42 | 43 | ## License 44 | 45 | Skyflo.ai is fully open source and licensed under the [Apache License 2.0](LICENSE). 46 | 47 | ## Community 48 | 49 | Join our community channels: 50 | 51 | - [Discord Server](https://discord.gg/kCFNavMund) 52 | - [GitHub Discussions](https://github.com/skyflo-ai/skyflo/discussions) 53 | - [Twitter/X](https://x.com/skyflo_ai) 54 | 55 | --- 56 | 57 | Thank you for contributing to Skyflo.ai! Your efforts help make cloud infrastructure management accessible through AI. 58 | -------------------------------------------------------------------------------- /ui/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Inter } from "next/font/google"; 4 | import "./globals.css"; 5 | import dynamic from "next/dynamic"; 6 | import ToastContainer from "@/components/ui/ToastContainer"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | const AuthProvider = dynamic( 11 | () => 12 | import("@/components/auth/AuthProvider").then((mod) => mod.AuthProvider), 13 | { ssr: false } 14 | ); 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | return ( 22 | 23 | 24 | Skyflo.ai | AI Agent for Cloud & DevOps 25 | 26 | 27 | {/* SVG filter used by the liquid glass effect */} 28 | 29 | 37 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 58 | 59 | 60 | 69 | 76 | 77 | 78 | {children} 79 | 80 | 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /ui/src/components/auth/Register.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import { Button } from "@/components/ui/button"; 6 | import { AuthInput } from "./AuthInput"; 7 | import { MdLock, MdEmail, MdPerson } from "react-icons/md"; 8 | import { useAuthStore } from "@/store/useAuthStore"; 9 | import { handleRegistration } from "@/lib/auth"; 10 | 11 | export const Register = () => { 12 | const { login } = useAuthStore(); 13 | const [loading, setLoading] = useState(false); 14 | const [error, setError] = useState(null); 15 | const [isMounted, setIsMounted] = useState(false); 16 | const router = useRouter(); 17 | 18 | useEffect(() => { 19 | setIsMounted(true); 20 | }, []); 21 | 22 | const handleSubmit = async (e: React.FormEvent) => { 23 | e.preventDefault(); 24 | if (!isMounted) return; 25 | 26 | setLoading(true); 27 | setError(null); 28 | 29 | const formData = new FormData(e.currentTarget); 30 | const result = await handleRegistration(formData); 31 | 32 | if (result && result.success) { 33 | router.push("/login"); 34 | } else { 35 | setError(result?.error || "Registration failed"); 36 | } 37 | 38 | setLoading(false); 39 | }; 40 | 41 | if (!isMounted) { 42 | return null; 43 | } 44 | 45 | return ( 46 |
47 | 54 | 61 | 68 | {error && ( 69 |
70 |

{error}

71 |
72 | )} 73 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /deployment/mcp/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use official Python image 2 | FROM python:3.11-slim 3 | 4 | ARG TARGETARCH 5 | 6 | # Install system dependencies 7 | RUN apt-get update && apt-get install -y --no-install-recommends \ 8 | git \ 9 | build-essential \ 10 | curl \ 11 | ca-certificates \ 12 | gnupg \ 13 | netcat-openbsd \ 14 | bash \ 15 | && apt-get clean \ 16 | && rm -rf /var/lib/apt/lists/* 17 | 18 | # Install kubectl 19 | RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/$TARGETARCH/kubectl" \ 20 | && chmod +x kubectl \ 21 | && mv kubectl /usr/local/bin/ 22 | 23 | # Install kubectl argo rollouts plugin 24 | RUN curl -LO "https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-$TARGETARCH" \ 25 | && chmod +x kubectl-argo-rollouts-linux-$TARGETARCH \ 26 | && mv kubectl-argo-rollouts-linux-$TARGETARCH /usr/local/bin/kubectl-argo-rollouts \ 27 | && ln -s /usr/local/bin/kubectl-argo-rollouts /usr/local/bin/kubectl-argo 28 | 29 | # Install Helm 30 | RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 \ 31 | && chmod 700 get_helm.sh \ 32 | && ./get_helm.sh \ 33 | && rm get_helm.sh 34 | 35 | # Create app user and directory 36 | RUN groupadd -g 1002 skyflogroup \ 37 | && useradd -u 1002 -g skyflogroup -s /bin/bash -m skyflo \ 38 | && mkdir -p /app \ 39 | && chown -R skyflo:skyflogroup /app 40 | 41 | # Set up application 42 | WORKDIR /app 43 | 44 | # Create and activate virtual environment early 45 | ENV VIRTUAL_ENV="/app/venv" 46 | RUN python -m venv $VIRTUAL_ENV 47 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 48 | 49 | # Copy dependency-related files first for better layer caching 50 | COPY mcp/pyproject.toml mcp/.python-version mcp/uv.lock mcp/README.md ./ 51 | COPY mcp/__about__.py ./ 52 | 53 | # Install dependencies 54 | RUN pip install --upgrade pip && \ 55 | pip install -e . && \ 56 | pip install uvicorn[standard] 57 | 58 | # Now copy all remaining application files 59 | COPY mcp/main.py ./ 60 | COPY mcp/config ./config 61 | COPY mcp/tools ./tools 62 | COPY mcp/utils ./utils 63 | 64 | # Copy and set up entrypoint script 65 | COPY deployment/mcp/entrypoint.sh /app/entrypoint.sh 66 | RUN chmod +x /app/entrypoint.sh 67 | 68 | # Set final permissions 69 | RUN chown -R skyflo:skyflogroup /app && \ 70 | chmod -R 755 /app 71 | 72 | # Update PATH and PYTHONPATH for skyflo user 73 | ENV PATH="/app/venv/bin:/usr/local/bin:/home/skyflo/.local/bin:${PATH}" \ 74 | PYTHONPATH="/app" 75 | 76 | # Expose the API port 77 | EXPOSE 8888 78 | 79 | LABEL org.opencontainers.image.source=https://github.com/skyflo-ai/skyflo 80 | LABEL org.opencontainers.image.description="Skyflo.ai MCP Server - Open Source AI Agent for Cloud & DevOps" 81 | 82 | # Switch to non-root user 83 | USER skyflo 84 | 85 | # Use the entrypoint script 86 | ENTRYPOINT ["/app/entrypoint.sh"] -------------------------------------------------------------------------------- /ui/src/app/welcome/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Register } from "@/components/auth/Register"; 4 | import { MdAdminPanelSettings } from "react-icons/md"; 5 | 6 | export default function Welcome() { 7 | return ( 8 |
9 |
10 | 15 |
16 | 17 |
23 | 24 |
25 |
26 | 27 |
28 |
29 |

30 |

31 | skyflo 32 | .ai 33 |

34 |

35 |

36 | Let's get started by creating your admin account 37 |

38 |
39 | 40 |
41 |
42 |
43 |
44 | 45 |
46 |

47 | Create Admin Account 48 |

49 |
50 |
51 |
52 | 53 |
54 |
55 | 56 |

57 | After creating your admin account, you'll be able to add team members 58 | from within the app. 59 |

60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /mcp/tests/tools/test_jenkins.py: -------------------------------------------------------------------------------- 1 | """Tests for tools.jenkins module.""" 2 | 3 | import pytest 4 | from tools.jenkins import ( 5 | _parse_credentials_ref, 6 | build_job_path 7 | ) 8 | 9 | 10 | class TestParseCredentialsRef: 11 | """Test cases for _parse_credentials_ref function.""" 12 | 13 | def test_parse_credentials_ref_valid(self): 14 | """Test parsing valid credentials reference.""" 15 | namespace, name = _parse_credentials_ref("default/my-secret") 16 | assert namespace == "default" 17 | assert name == "my-secret" 18 | 19 | def test_parse_credentials_ref_valid_with_hyphens(self): 20 | """Test parsing credentials reference with hyphens.""" 21 | namespace, name = _parse_credentials_ref("production/my-app-secret") 22 | assert namespace == "production" 23 | assert name == "my-app-secret" 24 | 25 | def test_parse_credentials_ref_invalid_format(self): 26 | """Test parsing invalid credentials reference format.""" 27 | with pytest.raises(ValueError, match="credentials_ref must be in the form 'namespace/name'"): 28 | _parse_credentials_ref("invalid-format") 29 | 30 | def test_parse_credentials_ref_empty_namespace(self): 31 | """Test parsing credentials reference with empty namespace.""" 32 | with pytest.raises(ValueError, match="credentials_ref components cannot be empty"): 33 | _parse_credentials_ref("/my-secret") 34 | 35 | def test_parse_credentials_ref_empty_name(self): 36 | """Test parsing credentials reference with empty name.""" 37 | with pytest.raises(ValueError, match="credentials_ref components cannot be empty"): 38 | _parse_credentials_ref("default/") 39 | 40 | def test_parse_credentials_ref_invalid_characters(self): 41 | """Test parsing credentials reference with invalid characters.""" 42 | with pytest.raises(ValueError, match="credentials_ref contains invalid characters"): 43 | _parse_credentials_ref("default/my_secret!") 44 | 45 | def test_parse_credentials_ref_colon_in_name(self): 46 | """Test parsing credentials reference with colon in name.""" 47 | with pytest.raises(ValueError, match="credentials_ref must be in the form 'namespace/name'"): 48 | _parse_credentials_ref("default/my:secret") 49 | 50 | 51 | class TestBuildJobPath: 52 | """Test cases for build_job_path function.""" 53 | 54 | def test_build_job_path_simple(self): 55 | """Test simple job path building.""" 56 | path = build_job_path("my-job") 57 | assert path == "/job/my-job" 58 | 59 | def test_build_job_path_nested(self): 60 | """Test nested job path building.""" 61 | path = build_job_path("folder/my-job") 62 | assert path == "/job/folder/job/my-job" 63 | 64 | def test_build_job_path_deeply_nested(self): 65 | """Test deeply nested job path building.""" 66 | path = build_job_path("a/b/c/my-job") 67 | assert path == "/job/a/job/b/job/c/job/my-job" 68 | -------------------------------------------------------------------------------- /ui/src/lib/approvals.ts: -------------------------------------------------------------------------------- 1 | import { getAuthHeaders } from "@/lib/api"; 2 | 3 | export interface ApprovalDecision { 4 | approve: boolean; 5 | reason?: string; 6 | conversation_id?: string; 7 | } 8 | 9 | export interface ApprovalResponse { 10 | status: string; 11 | call_id: string; 12 | approved: boolean; 13 | reason?: string; 14 | } 15 | 16 | export interface StopResponse { 17 | status: string; 18 | conversation_id: string; 19 | } 20 | 21 | const getApiBaseUrl = () => { 22 | return process.env.NEXT_PUBLIC_API_URL; 23 | }; 24 | 25 | const getClientAuthHeaders = async (): Promise> => { 26 | return { 27 | "Content-Type": "application/json", 28 | ...(await getAuthHeaders()), 29 | }; 30 | }; 31 | 32 | export const approveToolCall = async ( 33 | callId: string, 34 | reason?: string, 35 | conversationId?: string 36 | ): Promise => { 37 | const response = await fetch(`${getApiBaseUrl()}/agent/approvals/${callId}`, { 38 | method: "POST", 39 | headers: await getClientAuthHeaders(), 40 | credentials: "include", 41 | body: JSON.stringify({ 42 | approve: true, 43 | reason, 44 | conversation_id: conversationId, 45 | } satisfies ApprovalDecision), 46 | }); 47 | 48 | if (!response.ok) { 49 | const errorText = await response.text(); 50 | throw new Error( 51 | `Error approving tool call: ${response.statusText} - ${errorText}` 52 | ); 53 | } 54 | 55 | return await response.json(); 56 | }; 57 | 58 | export const denyToolCall = async ( 59 | callId: string, 60 | reason?: string, 61 | conversationId?: string 62 | ): Promise => { 63 | const response = await fetch(`${getApiBaseUrl()}/agent/approvals/${callId}`, { 64 | method: "POST", 65 | headers: await getClientAuthHeaders(), 66 | credentials: "include", 67 | body: JSON.stringify({ 68 | approve: false, 69 | reason, 70 | conversation_id: conversationId, 71 | } satisfies ApprovalDecision), 72 | }); 73 | 74 | if (!response.ok) { 75 | const errorText = await response.text(); 76 | throw new Error( 77 | `Error denying tool call: ${response.statusText} - ${errorText}` 78 | ); 79 | } 80 | 81 | return await response.json(); 82 | }; 83 | 84 | export const stopConversation = async ( 85 | conversationId: string, 86 | runId: string 87 | ): Promise => { 88 | const body = { 89 | conversation_id: conversationId, 90 | run_id: runId, 91 | }; 92 | 93 | const response = await fetch(`${getApiBaseUrl()}/agent/stop`, { 94 | method: "POST", 95 | headers: await getClientAuthHeaders(), 96 | credentials: "include", 97 | body: JSON.stringify(body), 98 | }); 99 | 100 | if (!response.ok) { 101 | const errorText = await response.text(); 102 | throw new Error( 103 | `Error stopping conversation: ${response.statusText} - ${errorText}` 104 | ); 105 | } 106 | 107 | return await response.json(); 108 | }; 109 | -------------------------------------------------------------------------------- /ui/src/app/api/conversation/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getAuthHeaders } from "@/lib/api"; 3 | 4 | export async function GET( 5 | request: Request, 6 | { params }: { params: { id: string } } 7 | ) { 8 | try { 9 | const headers = await getAuthHeaders(); 10 | 11 | const response = await fetch( 12 | `${process.env.API_URL}/conversations/${params.id}`, 13 | { 14 | method: "GET", 15 | headers, 16 | cache: "no-store", 17 | } 18 | ); 19 | 20 | if (!response.ok) { 21 | return NextResponse.json( 22 | { status: "error", error: "Failed to fetch conversation details" }, 23 | { status: response.status } 24 | ); 25 | } 26 | 27 | const data = await response.json(); 28 | 29 | return NextResponse.json(data); 30 | } catch (error) { 31 | return NextResponse.json( 32 | { status: "error", error: "Failed to fetch conversation details" }, 33 | { status: 500 } 34 | ); 35 | } 36 | } 37 | 38 | export async function PATCH( 39 | request: NextRequest, 40 | { params }: { params: { id: string } } 41 | ) { 42 | try { 43 | const headers = await getAuthHeaders(); 44 | const body = await request.json(); 45 | 46 | const response = await fetch( 47 | `${process.env.API_URL}/conversations/${params.id}`, 48 | { 49 | method: "PATCH", 50 | headers: { 51 | ...headers, 52 | "Content-Type": "application/json", 53 | }, 54 | body: JSON.stringify(body), 55 | cache: "no-store", 56 | } 57 | ); 58 | 59 | if (!response.ok) { 60 | return NextResponse.json( 61 | { status: "error", error: "Failed to update conversation" }, 62 | { status: response.status } 63 | ); 64 | } 65 | 66 | const data = await response.json(); 67 | return NextResponse.json(data); 68 | } catch (error) { 69 | return NextResponse.json( 70 | { status: "error", error: "Failed to update conversation" }, 71 | { status: 500 } 72 | ); 73 | } 74 | } 75 | 76 | export async function DELETE( 77 | request: NextRequest, 78 | { params }: { params: { id: string } } 79 | ) { 80 | try { 81 | const headers = await getAuthHeaders(); 82 | 83 | const response = await fetch( 84 | `${process.env.API_URL}/conversations/${params.id}`, 85 | { 86 | method: "DELETE", 87 | headers, 88 | cache: "no-store", 89 | } 90 | ); 91 | 92 | if (!response.ok) { 93 | return NextResponse.json( 94 | { status: "error", error: "Failed to delete conversation" }, 95 | { status: response.status } 96 | ); 97 | } 98 | 99 | const data = await response.json(); 100 | return NextResponse.json(data); 101 | } catch (error) { 102 | return NextResponse.json( 103 | { status: "error", error: "Failed to delete conversation" }, 104 | { status: 500 } 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /ui/src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Login } from "@/components/auth/Login"; 4 | import { useEffect, useState } from "react"; 5 | import { MdLockPerson } from "react-icons/md"; 6 | 7 | export default function LoginPage() { 8 | const [loading, setLoading] = useState(true); 9 | 10 | useEffect(() => { 11 | setLoading(false); 12 | }, []); 13 | 14 | if (loading) { 15 | return ( 16 |
17 |
18 |
19 | ); 20 | } 21 | 22 | return ( 23 |
24 |
25 | 30 |
31 | 32 |
38 | 39 |
40 |
41 | 42 |
43 |
44 |
45 |

46 | skyflo 47 | .ai 48 |

49 |
50 |

Sign in to your account

51 |
52 | 53 |
54 |
55 |
56 |
57 | 58 |
59 |

Sign In

60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skyflo.ai - AI Agent for Cloud & DevOps 2 | 3 |

4 | Skyflo.ai 5 |

6 | 7 | Skyflo.ai is your AI co-pilot for Cloud & DevOps that unifies Kubernetes operations and CI/CD systems (starting with Jenkins) through natural language with a safety-first, human-in-the-loop design. 8 | 9 | ## ⚡ Quick Start 10 | 11 | Install Skyflo.ai in your Kubernetes cluster using a single command: 12 | 13 | ```bash 14 | curl -sL https://raw.githubusercontent.com/skyflo-ai/skyflo/main/deployment/install.sh -o install.sh && chmod +x install.sh && ./install.sh 15 | ``` 16 | 17 | Skyflo can be configured to use different LLM providers (like OpenAI, Anthropic, Gemini, Groq, etc.), or even use a self-hosted model. 18 | 19 | See the [Installation Guide](https://github.com/skyflo-ai/skyflo/blob/main/docs/install.md) for details. 20 | 21 | ## 🚀 Key Features 22 | 23 | - **Unified AI Copilot**: One agent for K8s, Jenkins, Helm, and Argo Rollouts 24 | - **Human-in-the-loop Design**: Approval required for any mutating operation 25 | - **Plan → Execute → Verify**: Iterative loop where the agent keeps going untill the task is done 26 | - **Real-time Streaming**: Everything that the agent does is streamed to the UI in real time 27 | - **MCP-based tool execution**: Standardized tools for safe, consistent actions 28 | - **Built for Teams**: Manage teams, integrations, rate limiting and much more 29 | 30 | ## 🛠️ Supported Tools 31 | 32 | Skyflo.ai executes Cloud & DevOps operations through standardized tools and integrations: 33 | 34 | * **Kubernetes**: Resource discovery; get/describe; logs/exec; **safe apply/diff** flows. 35 | * **Argo Rollouts**: Inspect status; pause/resume; promote/cancel; analyze progressive delivery. 36 | * **Helm**: Search, install/upgrade/rollback with dry-run and diff-first safety. 37 | * **Jenkins (new)**: Jobs, builds, logs, SCM, identity—**secure auth & CSRF handling**, integration-aware tool filtering, and automatic parameter injection from configured credentials. 38 | 39 | Write/mutating operations require explicit approval from the user. 40 | 41 | ## 🎯 Who is Skyflo.ai for? 42 | 43 | Skyflo.ai is purpose-built for: 44 | 45 | - **DevOps Engineers** 46 | - **Cloud Architects** 47 | - **IT Managers** 48 | - **SRE Teams** 49 | - **Security Professionals** 50 | 51 | ## 🏗️ Architecture 52 | 53 | Read more about the architecture of Skyflo.ai in the [Architecture](docs/architecture.md) documentation. 54 | 55 | ## 🤝 Contributing 56 | 57 | We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) for details on getting started. 58 | 59 | ## 📜 Code of Conduct 60 | 61 | We have a [Code of Conduct](CODE_OF_CONDUCT.md) that we ask all contributors to follow. 62 | 63 | ## 🌐 Community 64 | 65 | - [Discord](https://discord.gg/kCFNavMund) 66 | - [Twitter/X](https://x.com/skyflo_ai) 67 | - [YouTube](https://www.youtube.com/@SkyfloAI) 68 | - [GitHub Discussions](https://github.com/skyflo-ai/skyflo/discussions) 69 | 70 | ## 📄 License 71 | 72 | Skyflo.ai is open source and licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). -------------------------------------------------------------------------------- /kubernetes-controller/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skyflo-ai/skyflo/kubernetes-controller 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | k8s.io/api v0.29.2 7 | k8s.io/apimachinery v0.29.2 8 | k8s.io/client-go v0.29.2 9 | sigs.k8s.io/controller-runtime v0.17.2 10 | ) 11 | 12 | require ( 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 17 | github.com/evanphx/json-patch/v5 v5.8.0 // indirect 18 | github.com/fsnotify/fsnotify v1.7.0 // indirect 19 | github.com/go-logr/logr v1.4.1 // indirect 20 | github.com/go-logr/zapr v1.3.0 // indirect 21 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 22 | github.com/go-openapi/jsonreference v0.20.2 // indirect 23 | github.com/go-openapi/swag v0.22.3 // indirect 24 | github.com/gogo/protobuf v1.3.2 // indirect 25 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 26 | github.com/golang/protobuf v1.5.3 // indirect 27 | github.com/google/gnostic-models v0.6.8 // indirect 28 | github.com/google/go-cmp v0.6.0 // indirect 29 | github.com/google/gofuzz v1.2.0 // indirect 30 | github.com/google/uuid v1.3.0 // indirect 31 | github.com/imdario/mergo v0.3.6 // indirect 32 | github.com/josharian/intern v1.0.0 // indirect 33 | github.com/json-iterator/go v1.1.12 // indirect 34 | github.com/mailru/easyjson v0.7.7 // indirect 35 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 36 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 37 | github.com/modern-go/reflect2 v1.0.2 // indirect 38 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 39 | github.com/pkg/errors v0.9.1 // indirect 40 | github.com/prometheus/client_golang v1.18.0 // indirect 41 | github.com/prometheus/client_model v0.5.0 // indirect 42 | github.com/prometheus/common v0.45.0 // indirect 43 | github.com/prometheus/procfs v0.12.0 // indirect 44 | github.com/spf13/pflag v1.0.5 // indirect 45 | go.uber.org/multierr v1.11.0 // indirect 46 | go.uber.org/zap v1.26.0 // indirect 47 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect 48 | golang.org/x/net v0.19.0 // indirect 49 | golang.org/x/oauth2 v0.12.0 // indirect 50 | golang.org/x/sys v0.16.0 // indirect 51 | golang.org/x/term v0.15.0 // indirect 52 | golang.org/x/text v0.14.0 // indirect 53 | golang.org/x/time v0.3.0 // indirect 54 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 55 | google.golang.org/appengine v1.6.7 // indirect 56 | google.golang.org/protobuf v1.31.0 // indirect 57 | gopkg.in/inf.v0 v0.9.1 // indirect 58 | gopkg.in/yaml.v2 v2.4.0 // indirect 59 | gopkg.in/yaml.v3 v3.0.1 // indirect 60 | k8s.io/apiextensions-apiserver v0.29.0 // indirect 61 | k8s.io/component-base v0.29.0 // indirect 62 | k8s.io/klog/v2 v2.110.1 // indirect 63 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect 64 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect 65 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 66 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 67 | sigs.k8s.io/yaml v1.4.0 // indirect 68 | ) 69 | -------------------------------------------------------------------------------- /.github/workflows/discord-gfi.yml: -------------------------------------------------------------------------------- 1 | name: Update good-first-issues list 2 | 3 | on: 4 | workflow_dispatch: 5 | # Re-build when labels or state change 6 | issues: 7 | types: [opened, reopened, edited, labeled, unlabeled, closed] 8 | # …and every day as a safety net 9 | schedule: 10 | - cron: '0 3 * * *' # 03:00 UTC daily 11 | 12 | jobs: 13 | refresh: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | # ---------- Build the embed ---------- 18 | - name: Build embed JSON (top-20) 19 | id: build 20 | uses: actions/github-script@v7 21 | with: 22 | script: | 23 | const { owner, repo } = context.repo; 24 | 25 | // pull first 20 open issues with that label 26 | const { data: issues } = await github.rest.issues.listForRepo({ 27 | owner, repo, 28 | state: 'open', 29 | labels: 'good first issue', 30 | per_page: 20 31 | }); 32 | 33 | // map to bullets: show other labels comma-separated after a hyphen 34 | const lines = issues.map(i => { 35 | const extras = i.labels 36 | .filter(l => l.name !== 'good first issue') 37 | .map(l => `\`${l.name}\``); 38 | const base = `• [#${i.number}](${i.html_url}) ${i.title}`; 39 | return extras.length 40 | ? `${base} - ${extras.join(', ')}` 41 | : base; 42 | }); 43 | 44 | const embed = { 45 | embeds: [{ 46 | title: '🆕 Good-first-issues', 47 | url: `https://github.com/${owner}/${repo}/issues?q=is%3Aissue+state%3Aopen+label%3A%22good+first+issue%22`, 48 | description: lines.join('\n'), 49 | color: 504575, 50 | timestamp: new Date().toISOString() 51 | }] 52 | }; 53 | 54 | // expose as a base-64 string 55 | core.setOutput( 56 | 'b64', 57 | Buffer.from(JSON.stringify(embed)).toString('base64') 58 | ); 59 | 60 | # ---------- Post via bot ---------- 61 | - name: Push embed (replace old message) 62 | env: 63 | BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} 64 | BOT_ID: ${{ secrets.DISCORD_BOT_ID }} 65 | CHANNEL_ID: ${{ secrets.DISCORD_CHANNEL_ID_GOOD_FIRST_ISSUES }} 66 | PAYLOAD_B64: ${{ steps.build.outputs.b64 }} 67 | run: | 68 | set -euo pipefail 69 | payload=$(printf '%s' "$PAYLOAD_B64" | base64 --decode) 70 | AUTH="Authorization: Bot ${BOT_TOKEN}" 71 | 72 | # 1) delete last bot message (if any) 73 | last=$(curl -sf -H "$AUTH" \ 74 | "https://discord.com/api/channels/${CHANNEL_ID}/messages?limit=50" | 75 | jq -r --arg BID "$BOT_ID" ' 76 | map(select(.author.id==$BID))[0].id // empty') 77 | if [ -n "$last" ]; then 78 | curl -s -X DELETE -H "$AUTH" \ 79 | "https://discord.com/api/channels/${CHANNEL_ID}/messages/${last}" >/dev/null 80 | fi 81 | 82 | # 2) post fresh embed 83 | curl -s -H "$AUTH" -H "Content-Type: application/json" \ 84 | -d "$payload" \ 85 | "https://discord.com/api/channels/${CHANNEL_ID}/messages" >/dev/null 86 | 87 | 88 | -------------------------------------------------------------------------------- /engine/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "skyflo-engine" 7 | dynamic = ["version"] 8 | description = "Skyflo.ai Engine Service - Middleware for Cloud Native AI Agent" 9 | readme = "README.md" 10 | requires-python = ">=3.11" 11 | license = "Apache-2.0" 12 | keywords = ["ai", "agent", "cloud native", "engine", "open source"] 13 | dependencies = [ 14 | "fastapi>=0.110.0", 15 | "uvicorn>=0.27.1", 16 | "pydantic>=2.10.6", 17 | "pydantic-settings>=2.2.1", 18 | "python-jose[cryptography]>=3.3.0", 19 | "passlib[bcrypt]>=1.7.4", 20 | "email-validator>=2.1.0.post1", 21 | "casbin>=1.41.0", 22 | "httpx>=0.27.0", 23 | "python-multipart>=0.0.9", 24 | "aiohttp>=3.11.14", 25 | "fastapi-websocket-pubsub>=0.3.9", 26 | "tortoise-orm>=0.24.2", 27 | "aerich==0.8.2", 28 | "broadcaster>=0.3.1", 29 | "redis>=5.0.1", 30 | "fastapi-limiter>=0.1.6", 31 | "langgraph>=0.3.18", 32 | "fastmcp>=2.12.3", 33 | "openai>=1.68.2", 34 | "typer>=0.15.2", 35 | "fastapi-users-tortoise>=0.2.0", 36 | "python-decouple>=3.8", 37 | "litellm>=1.67.4", 38 | "langgraph-checkpoint-postgres>=2.0.23", 39 | "psycopg[binary]>=3.1.0", 40 | "asyncpg>=0.30.0", 41 | ] 42 | 43 | [[project.authors]] 44 | name = "Karan Jagtiani" 45 | email = "karan@skyflo.ai" 46 | 47 | [project.optional-dependencies] 48 | default = ["pytest>=8.0.2", "pytest-cov>=4.1.0", "pytest-asyncio>=0.23.5", "pytest-mock>=3.12.0", "pytest-env>=1.1.3", "mypy>=1.8.0", "black>=24.2.0", "ruff>=0.3.0"] 49 | 50 | [project.scripts] 51 | skyflo-engine = "engine.main:run" 52 | 53 | [project.urls] 54 | Documentation = "https://github.com/skyflo-ai/skyflo#readme" 55 | Issues = "https://github.com/skyflo-ai/skyflo/issues" 56 | Source = "https://github.com/skyflo-ai/skyflo" 57 | 58 | [tool.hatch.version] 59 | path = "src/api/__about__.py" 60 | 61 | [tool.hatch.build.targets.wheel] 62 | packages = ["src/api"] 63 | 64 | [tool.hatch.envs.default] 65 | dependencies = ["pytest>=8.0.2", "pytest-cov>=4.1.0", "pytest-asyncio>=0.23.5", "pytest-mock>=3.12.0", "pytest-env>=1.1.3", "mypy>=1.8.0", "black>=24.2.0", "ruff>=0.3.0"] 66 | 67 | [tool.hatch.envs.default.scripts] 68 | test = "pytest {args:tests}" 69 | test-cov = "pytest --cov {args:src/api}" 70 | format = "black {args:src/api tests}" 71 | lint = "ruff check {args:src/api tests}" 72 | type-check = "mypy --install-types --non-interactive {args:src/api tests}" 73 | 74 | [tool.coverage.run] 75 | source_pkgs = ["api", "tests"] 76 | branch = true 77 | parallel = true 78 | omit = ["src/api/__about__.py"] 79 | 80 | [tool.coverage.paths] 81 | api = ["src/api", "*/api/src/api"] 82 | tests = ["tests", "*/api/tests"] 83 | 84 | [tool.mypy] 85 | python_version = "3.11" 86 | warn_return_any = true 87 | warn_unused_configs = true 88 | disallow_untyped_defs = true 89 | disallow_incomplete_defs = true 90 | 91 | [tool.ruff] 92 | target-version = "py311" 93 | line-length = 100 94 | select = ["E", "F", "B", "I"] 95 | 96 | [tool.black] 97 | line-length = 100 98 | target-version = ["py311"] 99 | 100 | [tool.aerich] 101 | tortoise_orm = "src.api.config.database.TORTOISE_ORM_CONFIG" 102 | location = "./migrations" 103 | src_folder = "./." 104 | -------------------------------------------------------------------------------- /ui/src/components/chat/PendingApprovalsBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, AnimatePresence } from "framer-motion"; 4 | import { AiOutlineStop } from "react-icons/ai"; 5 | 6 | import { MdAccessTime, MdDoneAll, MdClose } from "react-icons/md"; 7 | import { cn } from "@/lib/utils"; 8 | 9 | interface PendingApprovalsBarProps { 10 | count: number; 11 | onAction?: (decision: "approve" | "deny") => void; 12 | progress?: { 13 | done: number; 14 | total: number; 15 | decision: "approve" | "deny"; 16 | } | null; 17 | disabled?: boolean; 18 | } 19 | 20 | export function PendingApprovalsBar({ 21 | count, 22 | onAction, 23 | progress, 24 | disabled = false, 25 | }: PendingApprovalsBarProps) { 26 | const running = !!progress; 27 | const done = progress?.done ?? 0; 28 | const total = progress?.total ?? count; 29 | const decision = progress?.decision ?? "approve"; 30 | 31 | return ( 32 | 33 | 39 |
40 |
41 | 42 | 43 | {running 44 | ? `${ 45 | decision === "approve" ? "Approving" : "Declining" 46 | } ${Math.min(done + 1, total)}/${total}…` 47 | : `${count} approvals pending`} 48 | 49 |
50 | 51 |
52 | 70 | 86 |
87 |
88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /ui/src/types/events.ts: -------------------------------------------------------------------------------- 1 | export interface ToolExecutingEvent { 2 | type: "tool.executing"; 3 | call_id: string; 4 | tool: string; 5 | title: string; 6 | args: Record; 7 | run_id: string; 8 | timestamp: number; 9 | } 10 | 11 | export interface ToolResultEvent { 12 | type: "tool.result"; 13 | call_id: string; 14 | tool: string; 15 | title: string; 16 | result: Array<{ 17 | type: string; 18 | text?: string; 19 | annotations?: any; 20 | }>; 21 | run_id: string; 22 | timestamp: number; 23 | } 24 | 25 | export interface ToolAwaitingApprovalEvent { 26 | type: "tool.awaiting_approval"; 27 | run_id: string; 28 | call_id: string; 29 | tool: string; 30 | title: string; 31 | args: Record; 32 | context: Record; 33 | timestamp: number; 34 | } 35 | 36 | export interface ToolDeniedEvent { 37 | type: "tool.denied"; 38 | call_id: string; 39 | tool: string; 40 | title: string; 41 | args: Record; 42 | run_id: string; 43 | timestamp: number; 44 | } 45 | 46 | export interface ToolApprovedEvent { 47 | type: "tool.approved"; 48 | call_id: string; 49 | tool: string; 50 | title: string; 51 | args: Record; 52 | run_id: string; 53 | timestamp: number; 54 | } 55 | 56 | export interface ToolErrorEvent { 57 | type: "tool.error"; 58 | call_id: string; 59 | tool: string; 60 | title: string; 61 | error: string; 62 | run_id: string; 63 | timestamp: number; 64 | } 65 | 66 | export interface ToolsPendingEvent { 67 | type: "tools.pending"; 68 | run_id: string; 69 | tools: Array<{ 70 | call_id: string; 71 | tool: string; 72 | title: string; 73 | args: Record; 74 | requires_approval?: boolean; 75 | timestamp: number; 76 | }>; 77 | } 78 | 79 | export interface TokenEvent { 80 | type: "token"; 81 | text: string; 82 | conversation_id: string; 83 | } 84 | 85 | export interface TokenUsageEvent { 86 | type: "token.usage"; 87 | source: "turn_check" | "main"; 88 | model: string; 89 | prompt_tokens: number; 90 | completion_tokens: number; 91 | total_tokens: number; 92 | cached_tokens?: number; 93 | conversation_id: string; 94 | timestamp: number; 95 | } 96 | 97 | export interface TTFTEvent { 98 | type: "ttft"; 99 | duration: number; 100 | timestamp: number; 101 | run_id: string; 102 | } 103 | 104 | export interface ReadyEvent { 105 | type: "ready"; 106 | run_id: string; 107 | } 108 | 109 | export interface HeartbeatEvent { 110 | type: "heartbeat"; 111 | timestamp: string; 112 | } 113 | 114 | export interface ErrorEvent { 115 | type: "error"; 116 | error: string; 117 | } 118 | 119 | export interface CompletedEvent { 120 | type: "completed"; 121 | status: "completed" | "error"; 122 | run_id?: string; 123 | } 124 | 125 | export interface WorkflowCompleteEvent { 126 | type: "workflow_complete"; 127 | run_id: string; 128 | result?: any; 129 | status: "completed" | "awaiting_approval" | "stopped"; 130 | } 131 | 132 | export interface WorkflowErrorEvent { 133 | type: "workflow.error"; 134 | run_id: string; 135 | error: string; 136 | } 137 | 138 | export type Event = 139 | | ToolExecutingEvent 140 | | ToolResultEvent 141 | | ToolAwaitingApprovalEvent 142 | | ToolDeniedEvent 143 | | ToolApprovedEvent 144 | | ToolErrorEvent 145 | | ToolsPendingEvent 146 | | TokenEvent 147 | | TokenUsageEvent 148 | | TTFTEvent 149 | | ReadyEvent 150 | | HeartbeatEvent 151 | | ErrorEvent 152 | | CompletedEvent 153 | | WorkflowCompleteEvent 154 | | WorkflowErrorEvent; 155 | -------------------------------------------------------------------------------- /ui/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Link from "next/link"; 5 | import { motion } from "framer-motion"; 6 | 7 | export default function NotFound() { 8 | return ( 9 |
10 |
11 | 16 |
17 | 18 |
24 | 25 |
26 |
27 | 28 |
29 | 35 | 41 | 404 42 | 43 | 44 | 50 |

51 | Page Not Found 52 |

53 |
54 |
55 | 56 | 62 |

63 | Need help with cloud operations? 64 |

65 |
66 | 70 | Start a new chat 71 | 72 | 73 | 77 | View history 78 | 79 | 80 | 84 | Settings 85 | 86 |
87 |
88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /ui/src/components/settings/Settings.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | import TeamSettings from "./TeamSettings"; 6 | import ProfileSettings from "./ProfileSettings"; 7 | import { User } from "@/types/auth"; 8 | import { motion } from "framer-motion"; 9 | import { MdError } from "react-icons/md"; 10 | import { IoSettingsOutline } from "react-icons/io5"; 11 | 12 | const fadeInVariants = { 13 | hidden: { opacity: 0 }, 14 | visible: { opacity: 1, transition: { duration: 0.4 } }, 15 | }; 16 | 17 | interface SettingsProps { 18 | user: User | null; 19 | } 20 | 21 | export default function Settings({ user }: SettingsProps) { 22 | if (!user) { 23 | return ( 24 | 29 |
30 | 31 |

Authentication Required

32 |
33 |

34 | You need to be logged in to access profile settings. 35 |

36 |
37 | ); 38 | } 39 | 40 | return ( 41 | 47 | {/*
48 |
49 |
50 | 51 |
52 |
53 |

54 | Settings 55 |

56 |

57 | Manage your personal profile and team member access in a 58 | centralized dashboard. 59 |

60 |
{" "} 61 |
62 | 63 |
64 |
65 | 66 |
67 |
68 |
*/} 69 | 70 |
71 |
72 | 73 |
74 | 75 |
76 | 77 |
78 |
79 | 80 | 97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /engine/src/api/endpoints/integrations.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from fastapi import APIRouter, Depends, HTTPException, Query, status 4 | 5 | from ..config import rate_limit_dependency 6 | from ..models.integration import IntegrationRead, IntegrationCreate, IntegrationUpdate 7 | from ..services.integrations import IntegrationService 8 | from ..services.auth import current_active_user 9 | from ..models.user import User 10 | 11 | router = APIRouter() 12 | 13 | 14 | async def verify_admin_role(user: User = Depends(current_active_user)) -> User: 15 | if user.role != "admin": 16 | raise HTTPException( 17 | status_code=status.HTTP_403_FORBIDDEN, 18 | detail="Only administrators can perform this action", 19 | ) 20 | return user 21 | 22 | 23 | @router.post("/", response_model=IntegrationRead, dependencies=[rate_limit_dependency]) 24 | async def create_integration(payload: IntegrationCreate, user: User = Depends(verify_admin_role)): 25 | try: 26 | service = IntegrationService() 27 | created = await service.create_integration( 28 | created_by_user_id=str(user.id), 29 | provider=payload.provider, 30 | metadata=payload.metadata, 31 | credentials=payload.credentials, 32 | name=payload.name, 33 | ) 34 | return IntegrationRead.model_validate(created) 35 | except ValueError as ve: 36 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) 37 | except Exception as e: 38 | raise HTTPException( 39 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 40 | detail=f"Failed to create integration: {str(e)}", 41 | ) 42 | 43 | 44 | @router.get("/", response_model=List[IntegrationRead], dependencies=[rate_limit_dependency]) 45 | async def list_integrations( 46 | provider: Optional[str] = Query(default=None), user: User = Depends(current_active_user) 47 | ): 48 | try: 49 | service = IntegrationService() 50 | items = await service.list_integrations(provider=provider) 51 | return [IntegrationRead.model_validate(i) for i in items] 52 | except Exception as e: 53 | raise HTTPException( 54 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 55 | detail=f"Failed to list integrations: {str(e)}", 56 | ) 57 | 58 | 59 | @router.patch( 60 | "/{integration_id}", response_model=IntegrationRead, dependencies=[rate_limit_dependency] 61 | ) 62 | async def update_integration( 63 | integration_id: str, payload: IntegrationUpdate, user: User = Depends(verify_admin_role) 64 | ): 65 | try: 66 | service = IntegrationService() 67 | updated = await service.update_integration( 68 | integration_id=integration_id, 69 | metadata=payload.metadata, 70 | credentials=payload.credentials, 71 | name=payload.name, 72 | status=payload.status, 73 | ) 74 | return IntegrationRead.model_validate(updated) 75 | except Exception as e: 76 | raise HTTPException( 77 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 78 | detail=f"Failed to update integration: {str(e)}", 79 | ) 80 | 81 | 82 | @router.delete( 83 | "/{integration_id}", 84 | status_code=status.HTTP_204_NO_CONTENT, 85 | dependencies=[rate_limit_dependency], 86 | ) 87 | async def delete_integration( 88 | integration_id: str, 89 | user: User = Depends(verify_admin_role), 90 | ): 91 | try: 92 | service = IntegrationService() 93 | await service.delete_integration(integration_id=integration_id) 94 | return None 95 | except Exception as e: 96 | raise HTTPException( 97 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 98 | detail=f"Failed to delete integration: {str(e)}", 99 | ) 100 | -------------------------------------------------------------------------------- /engine/src/api/models/conversation.py: -------------------------------------------------------------------------------- 1 | """Conversation and Message models for chat history.""" 2 | 3 | import uuid 4 | from typing import List, Optional, Dict, Any 5 | from datetime import datetime 6 | 7 | from pydantic import BaseModel 8 | from tortoise import fields 9 | from tortoise.models import Model 10 | 11 | 12 | class Conversation(Model): 13 | """Conversation model for tracking chat sessions.""" 14 | 15 | id = fields.UUIDField(pk=True, default=uuid.uuid4) 16 | title = fields.CharField(max_length=255, null=True) 17 | user = fields.ForeignKeyField("models.User", related_name="conversations") 18 | is_active = fields.BooleanField(default=True) 19 | conversation_metadata = fields.JSONField(null=True) 20 | messages_json = fields.JSONField(null=True) 21 | created_at = fields.DatetimeField(auto_now_add=True) 22 | updated_at = fields.DatetimeField(auto_now=True) 23 | 24 | messages = fields.ReverseRelation["Message"] 25 | 26 | class Meta: 27 | """Tortoise ORM model configuration.""" 28 | 29 | table = "conversations" 30 | 31 | def __str__(self) -> str: 32 | """String representation of the conversation.""" 33 | return f"" 34 | 35 | 36 | class Message(Model): 37 | """Message model for storing individual chat messages.""" 38 | 39 | id = fields.UUIDField(pk=True, default=uuid.uuid4) 40 | conversation = fields.ForeignKeyField("models.Conversation", related_name="messages") 41 | role = fields.CharField(max_length=50) 42 | content = fields.TextField() 43 | sequence = fields.IntField() 44 | message_metadata = fields.JSONField(null=True) 45 | created_at = fields.DatetimeField(auto_now_add=True) 46 | 47 | class Meta: 48 | """Tortoise ORM model configuration.""" 49 | 50 | table = "messages" 51 | 52 | def __str__(self) -> str: 53 | """String representation of the message.""" 54 | return f"" 55 | 56 | 57 | class TokenUsageMetrics(BaseModel): 58 | """Captured token and latency metrics for an assistant response.""" 59 | 60 | prompt_tokens: int = 0 61 | completion_tokens: int = 0 62 | total_tokens: int = 0 63 | cached_tokens: int = 0 64 | cost: float = 0.0 65 | ttft_ms: Optional[int] = None 66 | ttr_ms: Optional[int] = None 67 | 68 | 69 | class MessageCreate(BaseModel): 70 | """Schema for message creation.""" 71 | 72 | role: str 73 | content: str 74 | sequence: int 75 | message_metadata: Optional[Dict[str, Any]] = None 76 | 77 | 78 | class MessageRead(BaseModel): 79 | """Schema for reading message data.""" 80 | 81 | id: uuid.UUID 82 | role: str 83 | content: str 84 | sequence: int 85 | message_metadata: Optional[Dict[str, Any]] = None 86 | token_usage: Optional[TokenUsageMetrics] = None 87 | created_at: datetime 88 | 89 | class Config: 90 | """Pydantic model configuration.""" 91 | 92 | from_attributes = True 93 | 94 | 95 | class ConversationCreate(BaseModel): 96 | """Schema for conversation creation.""" 97 | 98 | title: Optional[str] = None 99 | conversation_metadata: Optional[Dict[str, Any]] = None 100 | 101 | 102 | class ConversationRead(BaseModel): 103 | """Schema for reading conversation data.""" 104 | 105 | id: uuid.UUID 106 | title: Optional[str] 107 | user_id: uuid.UUID 108 | is_active: bool 109 | conversation_metadata: Optional[Dict[str, Any]] = None 110 | messages_json: Optional[List[Dict[str, Any]]] = None 111 | created_at: datetime 112 | updated_at: datetime 113 | messages: Optional[List[MessageRead]] = None 114 | 115 | class Config: 116 | """Pydantic model configuration.""" 117 | 118 | from_attributes = True 119 | 120 | 121 | class ConversationUpdate(BaseModel): 122 | """Schema for updating conversation data.""" 123 | 124 | title: Optional[str] = None 125 | is_active: Optional[bool] = None 126 | conversation_metadata: Optional[Dict[str, Any]] = None 127 | messages_json: Optional[List[Dict[str, Any]]] = None 128 | -------------------------------------------------------------------------------- /mcp/tests/utils/test_commands.py: -------------------------------------------------------------------------------- 1 | """Tests for utils.commands module.""" 2 | 3 | import pytest 4 | from utils.commands import run_command 5 | 6 | 7 | class TestRunCommand: 8 | """Test cases for run_command function.""" 9 | 10 | @pytest.mark.asyncio 11 | async def test_run_command_success(self, mocker): 12 | """Test successful command execution.""" 13 | mock_subprocess = mocker.patch('asyncio.create_subprocess_exec') 14 | mock_proc = mocker.AsyncMock() 15 | mock_proc.returncode = 0 16 | mock_proc.communicate = mocker.AsyncMock(return_value=(b"success output", b"")) 17 | mock_subprocess.return_value = mock_proc 18 | 19 | result = await run_command("test_cmd", ["arg1", "arg2"]) 20 | 21 | assert result["output"] == "success output" 22 | assert result["error"] is False 23 | mock_subprocess.assert_called_once_with( 24 | "test_cmd", "arg1", "arg2", 25 | stdout=-1, 26 | stderr=-1, 27 | stdin=None 28 | ) 29 | 30 | @pytest.mark.asyncio 31 | async def test_run_command_with_stdin(self, mocker): 32 | """Test command execution with stdin input.""" 33 | mock_subprocess = mocker.patch('asyncio.create_subprocess_exec') 34 | mock_proc = mocker.AsyncMock() 35 | mock_proc.returncode = 0 36 | mock_proc.communicate = mocker.AsyncMock(return_value=(b"output with stdin", b"")) 37 | mock_subprocess.return_value = mock_proc 38 | 39 | result = await run_command("test_cmd", ["arg1"], stdin="test input") 40 | 41 | assert result["output"] == "output with stdin" 42 | assert result["error"] is False 43 | mock_proc.communicate.assert_called_once_with(input=b"test input") 44 | 45 | @pytest.mark.asyncio 46 | async def test_run_command_error_return_code(self, mocker): 47 | """Test command execution with error return code.""" 48 | mock_subprocess = mocker.patch('asyncio.create_subprocess_exec') 49 | mock_proc = mocker.AsyncMock() 50 | mock_proc.returncode = 1 51 | mock_proc.communicate = mocker.AsyncMock(return_value=(b"", b"error message")) 52 | mock_subprocess.return_value = mock_proc 53 | 54 | result = await run_command("test_cmd", ["arg1"]) 55 | 56 | assert "Error executing command" in result["output"] 57 | assert result["error"] is True 58 | 59 | @pytest.mark.asyncio 60 | async def test_run_command_stderr_output(self, mocker): 61 | """Test command execution with stderr output but success return code.""" 62 | mock_subprocess = mocker.patch('asyncio.create_subprocess_exec') 63 | mock_proc = mocker.AsyncMock() 64 | mock_proc.returncode = 0 65 | mock_proc.communicate = mocker.AsyncMock(return_value=(b"", b"warning message")) 66 | mock_subprocess.return_value = mock_proc 67 | 68 | result = await run_command("test_cmd", ["arg1"]) 69 | 70 | assert result["output"] == "warning message" 71 | assert result["error"] is True 72 | 73 | @pytest.mark.asyncio 74 | async def test_run_command_no_output(self, mocker): 75 | """Test command execution with no output.""" 76 | mock_subprocess = mocker.patch('asyncio.create_subprocess_exec') 77 | mock_proc = mocker.AsyncMock() 78 | mock_proc.returncode = 0 79 | mock_proc.communicate = mocker.AsyncMock(return_value=(b"", b"")) 80 | mock_subprocess.return_value = mock_proc 81 | 82 | result = await run_command("test_cmd", ["arg1"]) 83 | 84 | assert "successfully" in result["output"] 85 | assert result["error"] is False 86 | 87 | @pytest.mark.asyncio 88 | async def test_run_command_exception(self, mocker): 89 | """Test command execution with exception.""" 90 | mock_subprocess = mocker.patch('asyncio.create_subprocess_exec') 91 | mock_subprocess.side_effect = Exception("Process creation failed") 92 | 93 | result = await run_command("test_cmd", ["arg1"]) 94 | 95 | assert "Error executing command" in result["output"] 96 | assert "Process creation failed" in result["output"] 97 | assert result["error"] is True 98 | -------------------------------------------------------------------------------- /ui/src/components/chat/Welcome.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useState } from "react"; 5 | import { motion } from "framer-motion"; 6 | import ChatHeader from "./ChatHeader"; 7 | import ChatSuggestions from "./ChatSuggestions"; 8 | import { ChatInput } from "./ChatInput"; 9 | import Loader from "@/components/ui/Loader"; 10 | 11 | export function Welcome() { 12 | const router = useRouter(); 13 | const [inputValue, setInputValue] = useState(""); 14 | const [isInitializing, setIsInitializing] = useState(false); 15 | const [error, setError] = useState(null); 16 | 17 | const initializeConversation = async (message: string) => { 18 | try { 19 | setError(null); 20 | setIsInitializing(true); 21 | 22 | const resp = await fetch("/api/conversation", { 23 | method: "POST", 24 | headers: { "Content-Type": "application/json" }, 25 | body: JSON.stringify({}), 26 | }); 27 | 28 | const data = await resp.json(); 29 | if (resp.ok && data?.id) { 30 | sessionStorage.setItem(`initialMessage:${data.id}`, message); 31 | router.push(`/chat/${data.id}`); 32 | return; 33 | } 34 | } catch (err) { 35 | setError("Failed to start conversation. Please try again."); 36 | setIsInitializing(false); 37 | } 38 | }; 39 | 40 | const handleSuggestionClick = (suggestionText: string) => { 41 | setInputValue(suggestionText); 42 | setTimeout(() => { 43 | const textArea = document.querySelector("textarea"); 44 | if (textArea) { 45 | textArea.focus(); 46 | textArea.setSelectionRange( 47 | suggestionText.length, 48 | suggestionText.length 49 | ); 50 | } 51 | }, 100); 52 | }; 53 | 54 | const handleSubmit = () => { 55 | if (!inputValue.trim()) return; 56 | initializeConversation(inputValue); 57 | setInputValue(""); 58 | }; 59 | 60 | return ( 61 |
62 | {isInitializing ? ( 63 |
64 | 69 | 70 | 71 |
72 | ) : ( 73 | <> 74 | {error && ( 75 |
76 | 81 | {error} 82 | 83 |
84 | )} 85 | 86 |
87 |
88 | 94 | 95 | 96 | 97 | 103 | 110 | 111 |
112 |
113 | 114 |
115 | 116 | )} 117 |
118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /ui/src/components/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Image from "next/image"; 5 | import { MdHistory, MdLogout, MdSettings, MdAdd } from "react-icons/md"; 6 | import { FaGithub } from "react-icons/fa"; 7 | import { FiLayers } from "react-icons/fi"; 8 | 9 | import { useAuth } from "@/components/auth/AuthProvider"; 10 | import { useAuthStore } from "@/store/useAuthStore"; 11 | import { handleLogout } from "@/lib/auth"; 12 | import { useRouter, usePathname } from "next/navigation"; 13 | import { 14 | Tooltip, 15 | TooltipContent, 16 | TooltipTrigger, 17 | TooltipProvider, 18 | } from "@/components/ui/tooltip"; 19 | 20 | export default function Navbar() { 21 | const router = useRouter(); 22 | const pathname = usePathname(); 23 | const { logout } = useAuth(); 24 | const { logout: storeLogout } = useAuthStore(); 25 | 26 | const handleLogoutOnClick = async () => { 27 | await handleLogout(); 28 | storeLogout(); 29 | logout(); 30 | router.push("/login"); 31 | }; 32 | 33 | return ( 34 | 94 | ); 95 | } 96 | 97 | function NavIcon({ 98 | icon, 99 | tooltip, 100 | onClick, 101 | isActive, 102 | }: { 103 | icon: React.ReactNode; 104 | tooltip: string; 105 | onClick?: () => void; 106 | isActive?: boolean; 107 | }) { 108 | return ( 109 | 110 | 111 | 112 | 123 | 124 | 125 |

{tooltip}

126 |
127 |
128 |
129 | ); 130 | } 131 | -------------------------------------------------------------------------------- /engine/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | env.bak/ 136 | venv.bak/ 137 | 138 | # Spyder project settings 139 | .spyderproject 140 | .spyproject 141 | 142 | # Rope project settings 143 | .ropeproject 144 | 145 | # mkdocs documentation 146 | /site 147 | 148 | # mypy 149 | .mypy_cache/ 150 | .dmypy.json 151 | dmypy.json 152 | 153 | # Pyre type checker 154 | .pyre/ 155 | 156 | # pytype static type analyzer 157 | .pytype/ 158 | 159 | # Cython debug symbols 160 | cython_debug/ 161 | 162 | # PyCharm 163 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 164 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 165 | # and can be added to the global gitignore or merged into this file. For a more nuclear 166 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 167 | #.idea/ 168 | 169 | # Ruff stuff: 170 | .ruff_cache/ 171 | 172 | # PyPI configuration file 173 | .pypirc -------------------------------------------------------------------------------- /mcp/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .env.*.local 133 | .venv 134 | env/ 135 | venv/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # Ruff stuff: 171 | .ruff_cache/ 172 | 173 | # PyPI configuration file 174 | .pypirc 175 | 176 | -------------------------------------------------------------------------------- /engine/src/api/services/title_generator.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import re 4 | from typing import Any, Dict, List, Optional 5 | 6 | from pydantic import BaseModel, Field 7 | from litellm import acompletion 8 | 9 | from ..config import settings 10 | from ..models.conversation import Conversation 11 | from ..agent.prompts import CHAT_TITLE_PROMPT 12 | from ..utils.helpers import get_api_key_for_provider 13 | from ..services.conversation_persistence import ConversationPersistenceService 14 | 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class TitleDecision(BaseModel): 20 | title: str = Field(..., min_length=1, max_length=60) 21 | 22 | 23 | def _clean_text_for_title(text: str) -> str: 24 | cleaned = re.sub(r"[\n\r\t]", " ", text or "").strip() 25 | cleaned = re.sub(r"\s+", " ", cleaned) 26 | cleaned = re.sub(r'[\.!?,;:"\'`]+', "", cleaned) 27 | return cleaned.strip() 28 | 29 | 30 | async def generate_chat_title( 31 | messages: List[Dict[str, Any]], model: str, api_key: Optional[str] = None 32 | ) -> str: 33 | curated = messages[-6:] if len(messages) > 6 else messages 34 | judge_messages = curated + [{"role": "user", "content": CHAT_TITLE_PROMPT}] 35 | 36 | completion_kwargs: Dict[str, Any] = { 37 | "model": model, 38 | "messages": judge_messages, 39 | "response_format": TitleDecision, 40 | "temperature": 0.2, 41 | "max_tokens": 64, 42 | } 43 | if api_key: 44 | completion_kwargs["api_key"] = api_key 45 | if getattr(settings, "LLM_HOST", None): 46 | completion_kwargs["api_base"] = settings.LLM_HOST 47 | 48 | try: 49 | resp = await acompletion(**completion_kwargs) 50 | parsed = TitleDecision.model_validate_json(resp.choices[0].message.content) 51 | return _clean_text_for_title(parsed.title) 52 | except Exception: 53 | fallback = "" 54 | for msg in reversed(curated): 55 | if isinstance(msg, dict) and msg.get("role") == "user": 56 | fallback = str(msg.get("content", "")) 57 | break 58 | if not fallback: 59 | return "New Conversation" 60 | cleaned = _clean_text_for_title(fallback) 61 | words = cleaned.split() 62 | return " ".join(words[:6]) or "New Conversation" 63 | 64 | 65 | async def generate_and_store_title( 66 | conversation_id: str, 67 | persistence: ConversationPersistenceService, 68 | ) -> None: 69 | try: 70 | model = settings.LLM_MODEL 71 | provider = model.split("/")[0] if "/" in model else "openai" 72 | api_key: Optional[str] = get_api_key_for_provider(provider) 73 | 74 | llm_messages: List[Dict[str, Any]] = [] 75 | assistant_seen = False 76 | 77 | for _ in range(16): 78 | conversation = await Conversation.get(id=conversation_id) 79 | if conversation.title: 80 | return 81 | try: 82 | llm_messages = await persistence.build_llm_messages_for_title_generation( 83 | conversation 84 | ) 85 | except Exception: 86 | llm_messages = [] 87 | 88 | if any( 89 | isinstance(m, dict) 90 | and m.get("role") == "assistant" 91 | and str(m.get("content", "")).strip() 92 | for m in llm_messages 93 | ): 94 | assistant_seen = True 95 | break 96 | 97 | await asyncio.sleep(0.5) # ~8 seconds at 0.5s intervals 98 | 99 | if not assistant_seen: 100 | # Fallback to the most recent user message only 101 | latest_user: Optional[Dict[str, Any]] = None 102 | for m in reversed(llm_messages): 103 | if isinstance(m, dict) and m.get("role") == "user": 104 | latest_user = m 105 | break 106 | llm_messages = [latest_user] if latest_user else [] 107 | 108 | title = await generate_chat_title(llm_messages, model, api_key) 109 | title = _clean_text_for_title(title)[:60] 110 | if not title: 111 | return 112 | 113 | await persistence.set_title(conversation_id, title) 114 | except Exception as e: 115 | logger.error(f"Error in generate_and_store_title for {conversation_id}: {e}") 116 | -------------------------------------------------------------------------------- /engine/src/api/integrations/jenkins/jenkins.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from typing import Dict, List, Any, Optional 3 | from pathlib import Path 4 | 5 | 6 | def build_jenkins_secret_yaml(name: str, namespace: str, creds: Dict[str, str]) -> str: 7 | username = creds.get("username") or creds.get("user") 8 | api_token = creds.get("api_token") or creds.get("api-token") or creds.get("token") 9 | 10 | if not username or not api_token: 11 | raise ValueError("Jenkins credentials must include 'username' and 'api_token'") 12 | 13 | template_path = Path(__file__).parent / "secret.yaml" 14 | with open(template_path, "r") as f: 15 | template = f.read() 16 | 17 | resolved_yaml = template.format( 18 | name=name, namespace=namespace, username=username, api_token=api_token 19 | ) 20 | 21 | return resolved_yaml 22 | 23 | 24 | def _tool_has_jenkins_tag(tool: Dict[str, Any]) -> bool: 25 | try: 26 | tags = tool.get("tags") or [] 27 | if isinstance(tags, list) and "jenkins" in tags: 28 | return True 29 | meta = tool.get("meta", {}) or {} 30 | fastmcp = meta.get("_fastmcp", {}) or {} 31 | fm_tags = fastmcp.get("tags") or [] 32 | return isinstance(fm_tags, list) and "jenkins" in fm_tags 33 | except Exception: 34 | return False 35 | 36 | 37 | def _strip_jenkins_input_params(tool: Dict[str, Any]) -> Dict[str, Any]: 38 | updated = deepcopy(tool) 39 | input_schema = updated.get("inputSchema") or updated.get("input_schema") 40 | if not isinstance(input_schema, dict): 41 | return updated 42 | 43 | props = input_schema.get("properties", {}) or {} 44 | required = list(input_schema.get("required", []) or []) 45 | params_to_strip = ["api_url", "credentials_ref"] 46 | 47 | for p in params_to_strip: 48 | if p in props: 49 | props.pop(p, None) 50 | if p in required: 51 | required = [r for r in required if r != p] 52 | 53 | input_schema["properties"] = props 54 | if required: 55 | input_schema["required"] = required 56 | else: 57 | input_schema.pop("required", None) 58 | updated["inputSchema"] = input_schema 59 | return updated 60 | 61 | 62 | def filter_jenkins_tools( 63 | tools: List[Dict[str, Any]], 64 | integration_status: Optional[str] = None, 65 | is_configured: bool = False, 66 | ) -> List[Dict[str, Any]]: 67 | if not is_configured or integration_status == "disabled": 68 | return [t for t in tools if not _tool_has_jenkins_tag(t)] 69 | 70 | transformed: List[Dict[str, Any]] = [] 71 | for tool in tools: 72 | if _tool_has_jenkins_tag(tool): 73 | tool = _strip_jenkins_input_params(tool) 74 | transformed.append(tool) 75 | 76 | return transformed 77 | 78 | 79 | def _is_jenkins_tool(tool_metadata: Optional[Dict[str, Any]], tool_name: str) -> bool: 80 | if not tool_metadata: 81 | return tool_name.startswith("jenkins_") 82 | return _tool_has_jenkins_tag(tool_metadata) or tool_name.startswith("jenkins_") 83 | 84 | 85 | def inject_jenkins_metadata_tool_args( 86 | tool_name: str, 87 | args: Dict[str, Any], 88 | tool_metadata: Optional[Dict[str, Any]], 89 | integration: Optional[Any] = None, 90 | ) -> tuple[Dict[str, Any], Optional[str]]: 91 | if not _is_jenkins_tool(tool_metadata, tool_name): 92 | return args, None 93 | 94 | if not integration or integration.status == "disabled": 95 | return ( 96 | args, 97 | "Jenkins integration is not configured. Admins can create one via /api/v1/integrations.", 98 | ) 99 | 100 | provided = dict(args or {}) 101 | meta = integration.metadata or {} 102 | 103 | if "api_url" not in provided and "api_url" in meta: 104 | provided["api_url"] = meta["api_url"] 105 | 106 | if "credentials_ref" not in provided and integration.credentials_ref: 107 | provided["credentials_ref"] = integration.credentials_ref 108 | 109 | return provided, None 110 | 111 | 112 | def strip_jenkins_metadata_tool_args(args: Dict[str, Any]) -> Dict[str, Any]: 113 | if not isinstance(args, dict): 114 | return args 115 | if not args: 116 | return args 117 | 118 | metadata_keys = ["api_url", "credentials_ref"] 119 | 120 | if not any(key in args for key in metadata_keys): 121 | return args 122 | 123 | sanitized = {k: v for k, v in args.items() if k not in metadata_keys} 124 | return sanitized 125 | --------------------------------------------------------------------------------