llms-py 3.0.23__py3-none-any.whl → 3.0.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llms/extensions/github_auth/README.md +169 -0
- llms/extensions/github_auth/__init__.py +254 -0
- llms/extensions/github_auth/ui/index.mjs +66 -0
- llms/extensions/skills/README.md +13 -6
- llms/extensions/skills/__init__.py +21 -16
- llms/extensions/skills/ui/index.mjs +15 -8
- llms/llms.json +0 -9
- llms/main.py +87 -294
- llms/ui/App.mjs +4 -1
- llms/ui/ai.mjs +1 -2
- llms/ui/modules/chat/ChatBody.mjs +1 -2
- llms/ui/modules/layout.mjs +1 -57
- {llms_py-3.0.23.dist-info → llms_py-3.0.25.dist-info}/METADATA +1 -1
- {llms_py-3.0.23.dist-info → llms_py-3.0.25.dist-info}/RECORD +18 -15
- {llms_py-3.0.23.dist-info → llms_py-3.0.25.dist-info}/WHEEL +0 -0
- {llms_py-3.0.23.dist-info → llms_py-3.0.25.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.23.dist-info → llms_py-3.0.25.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.23.dist-info → llms_py-3.0.25.dist-info}/top_level.txt +0 -0
|
@@ -3,6 +3,9 @@ import { leftPart } from "@servicestack/client"
|
|
|
3
3
|
|
|
4
4
|
let ext
|
|
5
5
|
|
|
6
|
+
const LLMS_HOME_SKILLS = "~/.llms/.agent/skills"
|
|
7
|
+
const LLMS_LOCAL_SKILLS = ".agent/skills"
|
|
8
|
+
|
|
6
9
|
const SkillSelector = {
|
|
7
10
|
template: `
|
|
8
11
|
<div class="px-4 py-4 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 max-h-[80vh] overflow-y-auto">
|
|
@@ -119,10 +122,10 @@ const SkillSelector = {
|
|
|
119
122
|
skills
|
|
120
123
|
}))
|
|
121
124
|
|
|
122
|
-
// Sort groups: writable (~/.llms/.
|
|
125
|
+
// Sort groups: writable (~/.llms/.agent/skills,.agent/skills) first, then alphabetically
|
|
123
126
|
definedGroups.sort((a, b) => {
|
|
124
|
-
const aEditable = a.name ===
|
|
125
|
-
const bEditable = b.name ===
|
|
127
|
+
const aEditable = a.name === LLMS_HOME_SKILLS || a.name === LLMS_LOCAL_SKILLS
|
|
128
|
+
const bEditable = b.name === LLMS_HOME_SKILLS || b.name === LLMS_LOCAL_SKILLS
|
|
126
129
|
if (aEditable !== bEditable) return aEditable ? -1 : 1
|
|
127
130
|
return a.name.localeCompare(b.name)
|
|
128
131
|
})
|
|
@@ -158,6 +161,10 @@ const SkillSelector = {
|
|
|
158
161
|
onlySkills = onlySkills.filter(s => s !== name)
|
|
159
162
|
} else {
|
|
160
163
|
onlySkills = [...onlySkills, name]
|
|
164
|
+
// If has all skills set to 'All' (null)
|
|
165
|
+
if (onlySkills.length === availableSkills.value.length) {
|
|
166
|
+
onlySkills = null
|
|
167
|
+
}
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
170
|
|
|
@@ -395,8 +402,8 @@ const SkillPage = {
|
|
|
395
402
|
grouped[group].push(skill)
|
|
396
403
|
})
|
|
397
404
|
return Object.entries(grouped).sort((a, b) => {
|
|
398
|
-
const aEditable = a[0] ===
|
|
399
|
-
const bEditable = b[0] ===
|
|
405
|
+
const aEditable = a[0] === LLMS_HOME_SKILLS || a[0] === LLMS_LOCAL_SKILLS
|
|
406
|
+
const bEditable = b[0] === LLMS_HOME_SKILLS || b[0] === LLMS_LOCAL_SKILLS
|
|
400
407
|
if (aEditable !== bEditable) return aEditable ? -1 : 1
|
|
401
408
|
return a[0].localeCompare(b[0])
|
|
402
409
|
}).map(([name, skills]) => ({ name, skills: skills.sort((a, b) => a.name.localeCompare(b.name)) }))
|
|
@@ -419,8 +426,8 @@ const SkillPage = {
|
|
|
419
426
|
return tree.sort((a, b) => { if (a.isFile !== b.isFile) return a.isFile ? 1 : -1; return a.name.localeCompare(b.name) })
|
|
420
427
|
}
|
|
421
428
|
const hasUnsavedChanges = computed(() => isEditing.value && editContent.value !== fileContent.value)
|
|
422
|
-
function isGroupEditable(groupName) { return groupName ===
|
|
423
|
-
function isEditable(skill) { return skill?.group ===
|
|
429
|
+
function isGroupEditable(groupName) { return groupName === LLMS_HOME_SKILLS || groupName === LLMS_LOCAL_SKILLS }
|
|
430
|
+
function isEditable(skill) { return skill?.group === LLMS_HOME_SKILLS || skill?.group === LLMS_LOCAL_SKILLS }
|
|
424
431
|
function isSkillExpanded(name) { return !!expandedSkills.value[name] }
|
|
425
432
|
function toggleSkillExpand(skill) {
|
|
426
433
|
expandedSkills.value[skill.name] = !expandedSkills.value[skill.name]
|
|
@@ -579,7 +586,7 @@ const SkillStore = {
|
|
|
579
586
|
<div class="h-full flex flex-col">
|
|
580
587
|
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between flex-shrink-0">
|
|
581
588
|
<div>
|
|
582
|
-
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
|
589
|
+
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">Discover Skills</h1>
|
|
583
590
|
<p class="text-sm text-gray-500 dark:text-gray-400">{{ total.toLocaleString() }} skills available</p>
|
|
584
591
|
</div>
|
|
585
592
|
<div class="flex items-center gap-2">
|
llms/llms.json
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"auth": {
|
|
4
|
-
"enabled": false,
|
|
5
|
-
"github": {
|
|
6
|
-
"client_id": "GITHUB_CLIENT_ID",
|
|
7
|
-
"client_secret": "GITHUB_CLIENT_SECRET",
|
|
8
|
-
"redirect_uri": "http://localhost:8000/auth/github/callback",
|
|
9
|
-
"restrict_to": "GITHUB_USERS"
|
|
10
|
-
}
|
|
11
|
-
},
|
|
12
3
|
"disable_extensions": [],
|
|
13
4
|
"defaults": {
|
|
14
5
|
"headers": {
|
llms/main.py
CHANGED
|
@@ -17,7 +17,6 @@ import json
|
|
|
17
17
|
import mimetypes
|
|
18
18
|
import os
|
|
19
19
|
import re
|
|
20
|
-
import secrets
|
|
21
20
|
import shlex
|
|
22
21
|
import shutil
|
|
23
22
|
import site
|
|
@@ -45,7 +44,7 @@ from typing import (
|
|
|
45
44
|
get_origin,
|
|
46
45
|
get_type_hints,
|
|
47
46
|
)
|
|
48
|
-
from urllib.parse import parse_qs,
|
|
47
|
+
from urllib.parse import parse_qs, urljoin
|
|
49
48
|
|
|
50
49
|
import aiohttp
|
|
51
50
|
from aiohttp import web
|
|
@@ -57,8 +56,8 @@ try:
|
|
|
57
56
|
except ImportError:
|
|
58
57
|
HAS_PIL = False
|
|
59
58
|
|
|
60
|
-
VERSION = "3.0.23"
|
|
61
59
|
_ROOT = None
|
|
60
|
+
VERSION = "3.0.25"
|
|
62
61
|
DEBUG = os.getenv("DEBUG") == "1"
|
|
63
62
|
MOCK = os.getenv("MOCK") == "1"
|
|
64
63
|
MOCK_DIR = os.getenv("MOCK_DIR")
|
|
@@ -70,8 +69,6 @@ g_handlers = {}
|
|
|
70
69
|
g_verbose = False
|
|
71
70
|
g_logprefix = ""
|
|
72
71
|
g_default_model = ""
|
|
73
|
-
g_sessions = {} # OAuth session storage: {session_token: {userId, userName, displayName, profileUrl, email, created}}
|
|
74
|
-
g_oauth_states = {} # CSRF protection: {state: {created, redirect_uri}}
|
|
75
72
|
g_app = None # ExtensionsContext Singleton
|
|
76
73
|
|
|
77
74
|
|
|
@@ -2790,8 +2787,36 @@ async def watch_config_files(config_path, providers_path, interval=1):
|
|
|
2790
2787
|
pass
|
|
2791
2788
|
|
|
2792
2789
|
|
|
2793
|
-
|
|
2794
|
-
|
|
2790
|
+
class AuthProvider:
|
|
2791
|
+
def __init__(self, app):
|
|
2792
|
+
self.app = app
|
|
2793
|
+
|
|
2794
|
+
def get_session_token(self, request: web.Request):
|
|
2795
|
+
return (
|
|
2796
|
+
request.query.get("session") or request.headers.get("X-Session-Token") or request.cookies.get("llms-token")
|
|
2797
|
+
)
|
|
2798
|
+
|
|
2799
|
+
def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
|
|
2800
|
+
session_token = self.get_session_token(request)
|
|
2801
|
+
|
|
2802
|
+
if not session_token or session_token not in self.app.sessions:
|
|
2803
|
+
return None
|
|
2804
|
+
|
|
2805
|
+
session_data = self.app.sessions[session_token]
|
|
2806
|
+
return session_data
|
|
2807
|
+
|
|
2808
|
+
def get_username(self, request: web.Request) -> Optional[str]:
|
|
2809
|
+
session = self.get_session(request)
|
|
2810
|
+
if session:
|
|
2811
|
+
return session.get("userName")
|
|
2812
|
+
return None
|
|
2813
|
+
|
|
2814
|
+
def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
2815
|
+
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
2816
|
+
session = self.get_session(request)
|
|
2817
|
+
if session:
|
|
2818
|
+
return True, session
|
|
2819
|
+
return False, None
|
|
2795
2820
|
|
|
2796
2821
|
|
|
2797
2822
|
class AppExtensions:
|
|
@@ -2824,6 +2849,9 @@ class AppExtensions:
|
|
|
2824
2849
|
self.index_headers = []
|
|
2825
2850
|
self.index_footers = []
|
|
2826
2851
|
self.allowed_directories = []
|
|
2852
|
+
self.auth_providers = []
|
|
2853
|
+
self.sessions = {} # OAuth session storage: {session_token: {userId, userName, displayName, profileUrl, email, created}}
|
|
2854
|
+
self.oauth_states = {} # CSRF protection: {state: {created, redirect_uri}}
|
|
2827
2855
|
self.request_args = {
|
|
2828
2856
|
"image_config": dict, # e.g. { "aspect_ratio": "1:1" }
|
|
2829
2857
|
"temperature": float, # e.g: 0.7
|
|
@@ -2879,7 +2907,6 @@ class AppExtensions:
|
|
|
2879
2907
|
|
|
2880
2908
|
def set_config(self, config: Dict[str, Any]):
|
|
2881
2909
|
self.config = config
|
|
2882
|
-
self.auth_enabled = self.config.get("auth", {}).get("enabled", False)
|
|
2883
2910
|
|
|
2884
2911
|
def set_allowed_directories(
|
|
2885
2912
|
self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
|
|
@@ -2897,40 +2924,38 @@ class AppExtensions:
|
|
|
2897
2924
|
"""Get the list of allowed directories."""
|
|
2898
2925
|
return self.allowed_directories
|
|
2899
2926
|
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
if not self.auth_enabled:
|
|
2904
|
-
return True, None
|
|
2905
|
-
|
|
2906
|
-
# Check for OAuth session token
|
|
2907
|
-
session_token = get_session_token(request)
|
|
2908
|
-
if session_token and session_token in g_sessions:
|
|
2909
|
-
return True, g_sessions[session_token]
|
|
2910
|
-
|
|
2911
|
-
# Check for API key
|
|
2912
|
-
auth_header = request.headers.get("Authorization", "")
|
|
2913
|
-
if auth_header.startswith("Bearer "):
|
|
2914
|
-
api_key = auth_header[7:]
|
|
2915
|
-
if api_key:
|
|
2916
|
-
return True, {"authProvider": "apikey"}
|
|
2927
|
+
def add_auth_provider(self, auth_provider: AuthProvider) -> None:
|
|
2928
|
+
"""Add an authentication provider."""
|
|
2929
|
+
self.auth_providers.append(auth_provider)
|
|
2917
2930
|
|
|
2918
|
-
|
|
2931
|
+
def is_auth_enabled(self) -> bool:
|
|
2932
|
+
return len(self.auth_providers) > 0
|
|
2919
2933
|
|
|
2920
2934
|
def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
|
|
2921
|
-
|
|
2935
|
+
for auth_provider in self.auth_providers:
|
|
2936
|
+
session = auth_provider.get_session(request)
|
|
2937
|
+
if session:
|
|
2938
|
+
return session
|
|
2939
|
+
return None
|
|
2922
2940
|
|
|
2923
|
-
|
|
2941
|
+
def get_username(self, request: web.Request) -> Optional[str]:
|
|
2942
|
+
for auth_provider in self.auth_providers:
|
|
2943
|
+
username = auth_provider.get_username(request)
|
|
2944
|
+
if username:
|
|
2945
|
+
return username
|
|
2924
2946
|
return None
|
|
2925
2947
|
|
|
2926
|
-
|
|
2927
|
-
|
|
2948
|
+
def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
2949
|
+
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
2950
|
+
if len(self.auth_providers) == 0:
|
|
2951
|
+
return True, None
|
|
2928
2952
|
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2953
|
+
for auth_provider in self.auth_providers:
|
|
2954
|
+
is_authenticated, user_data = auth_provider.check_auth(request)
|
|
2955
|
+
if is_authenticated:
|
|
2956
|
+
return True, user_data
|
|
2957
|
+
|
|
2958
|
+
return False, None
|
|
2934
2959
|
|
|
2935
2960
|
def get_user_path(self, username: Optional[str] = None) -> str:
|
|
2936
2961
|
if username:
|
|
@@ -3032,6 +3057,7 @@ def handler_name(handler):
|
|
|
3032
3057
|
class ExtensionContext:
|
|
3033
3058
|
def __init__(self, app: AppExtensions, path: str):
|
|
3034
3059
|
self.app = app
|
|
3060
|
+
self.config = app.config
|
|
3035
3061
|
self.cli_args = app.cli_args
|
|
3036
3062
|
self.extra_args = app.extra_args
|
|
3037
3063
|
self.error_auth_required = app.error_auth_required
|
|
@@ -3046,8 +3072,24 @@ class ExtensionContext:
|
|
|
3046
3072
|
self.verbose = g_verbose
|
|
3047
3073
|
self.aspect_ratios = app.aspect_ratios
|
|
3048
3074
|
self.request_args = app.request_args
|
|
3075
|
+
self.sessions = app.sessions
|
|
3076
|
+
self.oauth_states = app.oauth_states
|
|
3049
3077
|
self.disabled = False
|
|
3050
3078
|
|
|
3079
|
+
def add_auth_provider(self, auth_provider: AuthProvider) -> None:
|
|
3080
|
+
"""Add an authentication provider."""
|
|
3081
|
+
self.app.add_auth_provider(auth_provider)
|
|
3082
|
+
self.log(f"Added Auth Provider: {auth_provider.__class__.__name__}, Authentication is now enabled")
|
|
3083
|
+
|
|
3084
|
+
def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
|
|
3085
|
+
return self.app.get_session(request)
|
|
3086
|
+
|
|
3087
|
+
def get_username(self, request: web.Request) -> Optional[str]:
|
|
3088
|
+
return self.app.get_username(request)
|
|
3089
|
+
|
|
3090
|
+
def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
3091
|
+
return self.app.check_auth(request)
|
|
3092
|
+
|
|
3051
3093
|
def set_allowed_directories(
|
|
3052
3094
|
self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
|
|
3053
3095
|
) -> None:
|
|
@@ -3129,6 +3171,9 @@ class ExtensionContext:
|
|
|
3129
3171
|
def error_response(self, e: Exception, stacktrace: bool = False) -> Dict[str, Any]:
|
|
3130
3172
|
return to_error_response(e, stacktrace=stacktrace)
|
|
3131
3173
|
|
|
3174
|
+
def create_error_response(self, message, error_code="Error", stack_trace=None):
|
|
3175
|
+
return create_error_response(message, error_code, stack_trace)
|
|
3176
|
+
|
|
3132
3177
|
def add_provider(self, provider: Any):
|
|
3133
3178
|
self.log(f"Registered provider: {provider.__name__}")
|
|
3134
3179
|
self.app.all_providers.append(provider)
|
|
@@ -3983,33 +4028,11 @@ def cli_exec(cli_args, extra_args):
|
|
|
3983
4028
|
port = int(cli_args.serve)
|
|
3984
4029
|
|
|
3985
4030
|
# Validate auth configuration if enabled
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
# Expand environment variables
|
|
3993
|
-
if client_id.startswith("$"):
|
|
3994
|
-
client_id = client_id[1:]
|
|
3995
|
-
if client_secret.startswith("$"):
|
|
3996
|
-
client_secret = client_secret[1:]
|
|
3997
|
-
|
|
3998
|
-
client_id = os.getenv(client_id, client_id)
|
|
3999
|
-
client_secret = os.getenv(client_secret, client_secret)
|
|
4000
|
-
|
|
4001
|
-
if (
|
|
4002
|
-
not client_id
|
|
4003
|
-
or not client_secret
|
|
4004
|
-
or client_id == "GITHUB_CLIENT_ID"
|
|
4005
|
-
or client_secret == "GITHUB_CLIENT_SECRET"
|
|
4006
|
-
):
|
|
4007
|
-
print("ERROR: Authentication is enabled but GitHub OAuth is not properly configured.")
|
|
4008
|
-
print("Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables,")
|
|
4009
|
-
print("or disable authentication by setting 'auth.enabled' to false in llms.json")
|
|
4010
|
-
return ExitCode.FAILED
|
|
4011
|
-
|
|
4012
|
-
_log("Authentication enabled - GitHub OAuth configured")
|
|
4031
|
+
if g_config.get("auth", {}).get("enabled", False):
|
|
4032
|
+
print("ERROR: GitHub Authentication has moved to the github_auth extension.")
|
|
4033
|
+
print("Please remove the auth configuration from llms.json.")
|
|
4034
|
+
print("Learn more: https://llmspy.org/docs/deployment/github-oauth")
|
|
4035
|
+
return ExitCode.FAILED
|
|
4013
4036
|
|
|
4014
4037
|
client_max_size = g_config.get("limits", {}).get(
|
|
4015
4038
|
"client_max_size", 20 * 1024 * 1024
|
|
@@ -4226,236 +4249,6 @@ def cli_exec(cli_args, extra_args):
|
|
|
4226
4249
|
|
|
4227
4250
|
app.router.add_get("/~cache/{tail:.*}", cache_handler)
|
|
4228
4251
|
|
|
4229
|
-
# OAuth handlers
|
|
4230
|
-
async def github_auth_handler(request):
|
|
4231
|
-
"""Initiate GitHub OAuth flow"""
|
|
4232
|
-
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
4233
|
-
return web.json_response(create_error_response("GitHub OAuth not configured"), status=500)
|
|
4234
|
-
|
|
4235
|
-
auth_config = g_config["auth"]["github"]
|
|
4236
|
-
client_id = auth_config.get("client_id", "")
|
|
4237
|
-
redirect_uri = auth_config.get("redirect_uri", "")
|
|
4238
|
-
|
|
4239
|
-
# Expand environment variables
|
|
4240
|
-
if client_id.startswith("$"):
|
|
4241
|
-
client_id = client_id[1:]
|
|
4242
|
-
if redirect_uri.startswith("$"):
|
|
4243
|
-
redirect_uri = redirect_uri[1:]
|
|
4244
|
-
|
|
4245
|
-
client_id = os.getenv(client_id, client_id)
|
|
4246
|
-
redirect_uri = os.getenv(redirect_uri, redirect_uri)
|
|
4247
|
-
|
|
4248
|
-
if not client_id:
|
|
4249
|
-
return web.json_response(create_error_response("GitHub client_id not configured"), status=500)
|
|
4250
|
-
|
|
4251
|
-
# Generate CSRF state token
|
|
4252
|
-
state = secrets.token_urlsafe(32)
|
|
4253
|
-
g_oauth_states[state] = {"created": time.time(), "redirect_uri": redirect_uri}
|
|
4254
|
-
|
|
4255
|
-
# Clean up old states (older than 10 minutes)
|
|
4256
|
-
current_time = time.time()
|
|
4257
|
-
expired_states = [s for s, data in g_oauth_states.items() if current_time - data["created"] > 600]
|
|
4258
|
-
for s in expired_states:
|
|
4259
|
-
del g_oauth_states[s]
|
|
4260
|
-
|
|
4261
|
-
# Build GitHub authorization URL
|
|
4262
|
-
params = {
|
|
4263
|
-
"client_id": client_id,
|
|
4264
|
-
"redirect_uri": redirect_uri,
|
|
4265
|
-
"state": state,
|
|
4266
|
-
"scope": "read:user user:email",
|
|
4267
|
-
}
|
|
4268
|
-
auth_url = f"https://github.com/login/oauth/authorize?{urlencode(params)}"
|
|
4269
|
-
|
|
4270
|
-
return web.HTTPFound(auth_url)
|
|
4271
|
-
|
|
4272
|
-
def validate_user(github_username):
|
|
4273
|
-
auth_config = g_config["auth"]["github"]
|
|
4274
|
-
# Check if user is restricted
|
|
4275
|
-
restrict_to = auth_config.get("restrict_to", "")
|
|
4276
|
-
|
|
4277
|
-
# Expand environment variables
|
|
4278
|
-
if restrict_to.startswith("$"):
|
|
4279
|
-
restrict_to = restrict_to[1:]
|
|
4280
|
-
|
|
4281
|
-
restrict_to = os.getenv(restrict_to, None if restrict_to == "GITHUB_USERS" else restrict_to)
|
|
4282
|
-
|
|
4283
|
-
# If restrict_to is configured, validate the user
|
|
4284
|
-
if restrict_to:
|
|
4285
|
-
# Parse allowed users (comma or space delimited)
|
|
4286
|
-
allowed_users = [u.strip() for u in re.split(r"[,\s]+", restrict_to) if u.strip()]
|
|
4287
|
-
|
|
4288
|
-
# Check if user is in the allowed list
|
|
4289
|
-
if not github_username or github_username not in allowed_users:
|
|
4290
|
-
_log(f"Access denied for user: {github_username}. Not in allowed list: {allowed_users}")
|
|
4291
|
-
return web.Response(
|
|
4292
|
-
text=f"Access denied. User '{github_username}' is not authorized to access this application.",
|
|
4293
|
-
status=403,
|
|
4294
|
-
)
|
|
4295
|
-
return None
|
|
4296
|
-
|
|
4297
|
-
async def github_callback_handler(request):
|
|
4298
|
-
"""Handle GitHub OAuth callback"""
|
|
4299
|
-
code = request.query.get("code")
|
|
4300
|
-
state = request.query.get("state")
|
|
4301
|
-
|
|
4302
|
-
# Handle malformed URLs where query params are appended with & instead of ?
|
|
4303
|
-
if not code and "tail" in request.match_info:
|
|
4304
|
-
tail = request.match_info["tail"]
|
|
4305
|
-
if tail.startswith("&"):
|
|
4306
|
-
params = parse_qs(tail[1:])
|
|
4307
|
-
code = params.get("code", [None])[0]
|
|
4308
|
-
state = params.get("state", [None])[0]
|
|
4309
|
-
|
|
4310
|
-
if not code or not state:
|
|
4311
|
-
return web.Response(text="Missing code or state parameter", status=400)
|
|
4312
|
-
|
|
4313
|
-
# Verify state token (CSRF protection)
|
|
4314
|
-
if state not in g_oauth_states:
|
|
4315
|
-
return web.Response(text="Invalid state parameter", status=400)
|
|
4316
|
-
|
|
4317
|
-
g_oauth_states.pop(state)
|
|
4318
|
-
|
|
4319
|
-
if "auth" not in g_config or "github" not in g_config["auth"]:
|
|
4320
|
-
return web.json_response(create_error_response("GitHub OAuth not configured"), status=500)
|
|
4321
|
-
|
|
4322
|
-
auth_config = g_config["auth"]["github"]
|
|
4323
|
-
client_id = auth_config.get("client_id", "")
|
|
4324
|
-
client_secret = auth_config.get("client_secret", "")
|
|
4325
|
-
redirect_uri = auth_config.get("redirect_uri", "")
|
|
4326
|
-
|
|
4327
|
-
# Expand environment variables
|
|
4328
|
-
if client_id.startswith("$"):
|
|
4329
|
-
client_id = client_id[1:]
|
|
4330
|
-
if client_secret.startswith("$"):
|
|
4331
|
-
client_secret = client_secret[1:]
|
|
4332
|
-
if redirect_uri.startswith("$"):
|
|
4333
|
-
redirect_uri = redirect_uri[1:]
|
|
4334
|
-
|
|
4335
|
-
client_id = os.getenv(client_id, client_id)
|
|
4336
|
-
client_secret = os.getenv(client_secret, client_secret)
|
|
4337
|
-
redirect_uri = os.getenv(redirect_uri, redirect_uri)
|
|
4338
|
-
|
|
4339
|
-
if not client_id or not client_secret:
|
|
4340
|
-
return web.json_response(create_error_response("GitHub OAuth credentials not configured"), status=500)
|
|
4341
|
-
|
|
4342
|
-
# Exchange code for access token
|
|
4343
|
-
async with aiohttp.ClientSession() as session:
|
|
4344
|
-
token_url = "https://github.com/login/oauth/access_token"
|
|
4345
|
-
token_data = {
|
|
4346
|
-
"client_id": client_id,
|
|
4347
|
-
"client_secret": client_secret,
|
|
4348
|
-
"code": code,
|
|
4349
|
-
"redirect_uri": redirect_uri,
|
|
4350
|
-
}
|
|
4351
|
-
headers = {"Accept": "application/json"}
|
|
4352
|
-
|
|
4353
|
-
async with session.post(token_url, data=token_data, headers=headers) as resp:
|
|
4354
|
-
token_response = await resp.json()
|
|
4355
|
-
access_token = token_response.get("access_token")
|
|
4356
|
-
|
|
4357
|
-
if not access_token:
|
|
4358
|
-
error = token_response.get("error_description", "Failed to get access token")
|
|
4359
|
-
return web.json_response(create_error_response(f"OAuth error: {error}"), status=400)
|
|
4360
|
-
|
|
4361
|
-
# Fetch user info
|
|
4362
|
-
user_url = "https://api.github.com/user"
|
|
4363
|
-
headers = {"Authorization": f"Bearer {access_token}", "Accept": "application/json"}
|
|
4364
|
-
|
|
4365
|
-
async with session.get(user_url, headers=headers) as resp:
|
|
4366
|
-
user_data = await resp.json()
|
|
4367
|
-
|
|
4368
|
-
# Validate user
|
|
4369
|
-
error_response = validate_user(user_data.get("login", ""))
|
|
4370
|
-
if error_response:
|
|
4371
|
-
return error_response
|
|
4372
|
-
|
|
4373
|
-
# Create session
|
|
4374
|
-
session_token = secrets.token_urlsafe(32)
|
|
4375
|
-
g_sessions[session_token] = {
|
|
4376
|
-
"userId": str(user_data.get("id", "")),
|
|
4377
|
-
"userName": user_data.get("login", ""),
|
|
4378
|
-
"displayName": user_data.get("name", ""),
|
|
4379
|
-
"profileUrl": user_data.get("avatar_url", ""),
|
|
4380
|
-
"email": user_data.get("email", ""),
|
|
4381
|
-
"created": time.time(),
|
|
4382
|
-
}
|
|
4383
|
-
|
|
4384
|
-
# Redirect to UI with session token
|
|
4385
|
-
response = web.HTTPFound(f"/?session={session_token}")
|
|
4386
|
-
response.set_cookie("llms-token", session_token, httponly=True, path="/", max_age=86400)
|
|
4387
|
-
return response
|
|
4388
|
-
|
|
4389
|
-
async def session_handler(request):
|
|
4390
|
-
"""Validate and return session info"""
|
|
4391
|
-
session_token = get_session_token(request)
|
|
4392
|
-
|
|
4393
|
-
if not session_token or session_token not in g_sessions:
|
|
4394
|
-
return web.json_response(create_error_response("Invalid or expired session"), status=401)
|
|
4395
|
-
|
|
4396
|
-
session_data = g_sessions[session_token]
|
|
4397
|
-
|
|
4398
|
-
# Clean up old sessions (older than 24 hours)
|
|
4399
|
-
current_time = time.time()
|
|
4400
|
-
expired_sessions = [token for token, data in g_sessions.items() if current_time - data["created"] > 86400]
|
|
4401
|
-
for token in expired_sessions:
|
|
4402
|
-
del g_sessions[token]
|
|
4403
|
-
|
|
4404
|
-
return web.json_response({**session_data, "sessionToken": session_token})
|
|
4405
|
-
|
|
4406
|
-
async def logout_handler(request):
|
|
4407
|
-
"""End OAuth session"""
|
|
4408
|
-
session_token = get_session_token(request)
|
|
4409
|
-
|
|
4410
|
-
if session_token and session_token in g_sessions:
|
|
4411
|
-
del g_sessions[session_token]
|
|
4412
|
-
|
|
4413
|
-
response = web.json_response({"success": True})
|
|
4414
|
-
response.del_cookie("llms-token")
|
|
4415
|
-
return response
|
|
4416
|
-
|
|
4417
|
-
async def auth_handler(request):
|
|
4418
|
-
"""Check authentication status and return user info"""
|
|
4419
|
-
# Check for OAuth session token
|
|
4420
|
-
session_token = get_session_token(request)
|
|
4421
|
-
|
|
4422
|
-
if session_token and session_token in g_sessions:
|
|
4423
|
-
session_data = g_sessions[session_token]
|
|
4424
|
-
return web.json_response(
|
|
4425
|
-
{
|
|
4426
|
-
"userId": session_data.get("userId", ""),
|
|
4427
|
-
"userName": session_data.get("userName", ""),
|
|
4428
|
-
"displayName": session_data.get("displayName", ""),
|
|
4429
|
-
"profileUrl": session_data.get("profileUrl", ""),
|
|
4430
|
-
"authProvider": "github",
|
|
4431
|
-
}
|
|
4432
|
-
)
|
|
4433
|
-
|
|
4434
|
-
# Check for API key in Authorization header
|
|
4435
|
-
# auth_header = request.headers.get('Authorization', '')
|
|
4436
|
-
# if auth_header.startswith('Bearer '):
|
|
4437
|
-
# # For API key auth, return a basic response
|
|
4438
|
-
# # You can customize this based on your API key validation logic
|
|
4439
|
-
# api_key = auth_header[7:]
|
|
4440
|
-
# if api_key: # Add your API key validation logic here
|
|
4441
|
-
# return web.json_response({
|
|
4442
|
-
# "userId": "1",
|
|
4443
|
-
# "userName": "apiuser",
|
|
4444
|
-
# "displayName": "API User",
|
|
4445
|
-
# "profileUrl": "",
|
|
4446
|
-
# "authProvider": "apikey"
|
|
4447
|
-
# })
|
|
4448
|
-
|
|
4449
|
-
# Not authenticated - return error in expected format
|
|
4450
|
-
return web.json_response(g_app.error_auth_required, status=401)
|
|
4451
|
-
|
|
4452
|
-
app.router.add_get("/auth", auth_handler)
|
|
4453
|
-
app.router.add_get("/auth/github", github_auth_handler)
|
|
4454
|
-
app.router.add_get("/auth/github/callback", github_callback_handler)
|
|
4455
|
-
app.router.add_get("/auth/github/callback{tail:.*}", github_callback_handler)
|
|
4456
|
-
app.router.add_get("/auth/session", session_handler)
|
|
4457
|
-
app.router.add_post("/auth/logout", logout_handler)
|
|
4458
|
-
|
|
4459
4252
|
async def ui_static(request: web.Request) -> web.Response:
|
|
4460
4253
|
path = Path(request.match_info["path"])
|
|
4461
4254
|
|
|
@@ -4494,8 +4287,8 @@ def cli_exec(cli_args, extra_args):
|
|
|
4494
4287
|
enabled, disabled = provider_status()
|
|
4495
4288
|
ret["status"] = {"all": list(g_config["providers"].keys()), "enabled": enabled, "disabled": disabled}
|
|
4496
4289
|
# Add auth configuration
|
|
4497
|
-
ret["requiresAuth"] =
|
|
4498
|
-
ret["
|
|
4290
|
+
ret["requiresAuth"] = g_app.is_auth_enabled()
|
|
4291
|
+
ret["authTypes"] = [provider.__class__.__name__ for provider in g_app.auth_providers]
|
|
4499
4292
|
return web.json_response(ret)
|
|
4500
4293
|
|
|
4501
4294
|
app.router.add_get("/config", config_handler)
|
llms/ui/App.mjs
CHANGED
|
@@ -173,7 +173,10 @@ export default {
|
|
|
173
173
|
<div>
|
|
174
174
|
<ModelSelector :models="$state.models" v-model="$state.selectedModel" />
|
|
175
175
|
</div>
|
|
176
|
-
<
|
|
176
|
+
<div class="flex items-center gap-2">
|
|
177
|
+
<TopBar id="top-bar" />
|
|
178
|
+
<Avatar />
|
|
179
|
+
</div>
|
|
177
180
|
</div>
|
|
178
181
|
<TopPanel v-if="$ai.hasAccess" id="top-panel" :class="$ctx.cls('top-panel', 'shrink-0')" />
|
|
179
182
|
<div id="page" :class="$ctx.cls('page', 'flex-1 overflow-y-auto min-h-0 flex flex-col')">
|
llms/ui/ai.mjs
CHANGED
|
@@ -6,7 +6,7 @@ const headers = { 'Accept': 'application/json' }
|
|
|
6
6
|
const prefsKey = 'llms.prefs'
|
|
7
7
|
|
|
8
8
|
export const o = {
|
|
9
|
-
version: '3.0.
|
|
9
|
+
version: '3.0.25',
|
|
10
10
|
base,
|
|
11
11
|
prefsKey,
|
|
12
12
|
welcome: 'Welcome to llms.py',
|
|
@@ -165,7 +165,6 @@ export const o = {
|
|
|
165
165
|
return { config, models, extensions, auth }
|
|
166
166
|
},
|
|
167
167
|
|
|
168
|
-
|
|
169
168
|
async uploadFile(file) {
|
|
170
169
|
const formData = new FormData()
|
|
171
170
|
formData.append('file', file)
|
|
@@ -643,8 +643,7 @@ export const ChatBody = {
|
|
|
643
643
|
<div class="mx-auto max-w-6xl px-4 py-6">
|
|
644
644
|
|
|
645
645
|
<div v-if="!$ai.hasAccess">
|
|
646
|
-
<
|
|
647
|
-
<SignIn v-else @done="$ai.signIn($event)" />
|
|
646
|
+
<SignIn @done="$ai.signIn($event)" />
|
|
648
647
|
</div>
|
|
649
648
|
<!-- Welcome message when no thread is selected -->
|
|
650
649
|
<div v-else-if="!currentThread" class="text-center py-12">
|