├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
68 |
*/}
69 |
70 |
71 |
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 |
--------------------------------------------------------------------------------