minder-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- minder/__init__.py +12 -0
- minder/api/routers/prompts.py +177 -0
- minder/application/__init__.py +1 -0
- minder/application/admin/__init__.py +11 -0
- minder/application/admin/dto.py +453 -0
- minder/application/admin/jobs.py +327 -0
- minder/application/admin/use_cases.py +1895 -0
- minder/auth/__init__.py +12 -0
- minder/auth/context.py +26 -0
- minder/auth/middleware.py +70 -0
- minder/auth/principal.py +59 -0
- minder/auth/rate_limiter.py +89 -0
- minder/auth/rbac.py +60 -0
- minder/auth/service.py +541 -0
- minder/bootstrap/__init__.py +9 -0
- minder/bootstrap/providers.py +109 -0
- minder/bootstrap/transport.py +807 -0
- minder/cache/__init__.py +10 -0
- minder/cache/providers.py +140 -0
- minder/chunking/__init__.py +4 -0
- minder/chunking/code_splitter.py +184 -0
- minder/chunking/splitter.py +136 -0
- minder/cli.py +1542 -0
- minder/config.py +179 -0
- minder/continuity.py +363 -0
- minder/dev.py +160 -0
- minder/embedding/__init__.py +9 -0
- minder/embedding/base.py +7 -0
- minder/embedding/local.py +65 -0
- minder/embedding/openai.py +7 -0
- minder/graph/__init__.py +11 -0
- minder/graph/edges.py +13 -0
- minder/graph/executor.py +127 -0
- minder/graph/graph.py +263 -0
- minder/graph/nodes/__init__.py +27 -0
- minder/graph/nodes/evaluator.py +21 -0
- minder/graph/nodes/guard.py +64 -0
- minder/graph/nodes/llm.py +59 -0
- minder/graph/nodes/planning.py +30 -0
- minder/graph/nodes/reasoning.py +87 -0
- minder/graph/nodes/reranker.py +141 -0
- minder/graph/nodes/retriever.py +86 -0
- minder/graph/nodes/verification.py +230 -0
- minder/graph/nodes/workflow_planner.py +250 -0
- minder/graph/runtime.py +15 -0
- minder/graph/state.py +26 -0
- minder/llm/__init__.py +5 -0
- minder/llm/base.py +14 -0
- minder/llm/local.py +381 -0
- minder/llm/openai.py +89 -0
- minder/models/__init__.py +109 -0
- minder/models/base.py +10 -0
- minder/models/client.py +137 -0
- minder/models/document.py +34 -0
- minder/models/error.py +32 -0
- minder/models/graph.py +114 -0
- minder/models/history.py +32 -0
- minder/models/job.py +62 -0
- minder/models/prompt.py +41 -0
- minder/models/repository.py +62 -0
- minder/models/rule.py +68 -0
- minder/models/session.py +51 -0
- minder/models/skill.py +52 -0
- minder/models/user.py +41 -0
- minder/models/workflow.py +35 -0
- minder/observability/__init__.py +57 -0
- minder/observability/audit.py +243 -0
- minder/observability/logging.py +253 -0
- minder/observability/metrics.py +448 -0
- minder/observability/tracing.py +215 -0
- minder/presentation/__init__.py +1 -0
- minder/presentation/http/__init__.py +1 -0
- minder/presentation/http/admin/__init__.py +3 -0
- minder/presentation/http/admin/api.py +1309 -0
- minder/presentation/http/admin/context.py +94 -0
- minder/presentation/http/admin/dashboard.py +111 -0
- minder/presentation/http/admin/jobs.py +208 -0
- minder/presentation/http/admin/memories.py +185 -0
- minder/presentation/http/admin/prompts.py +219 -0
- minder/presentation/http/admin/routes.py +127 -0
- minder/presentation/http/admin/runtime.py +650 -0
- minder/presentation/http/admin/search.py +368 -0
- minder/presentation/http/admin/skills.py +230 -0
- minder/prompts/__init__.py +646 -0
- minder/prompts/formatter.py +142 -0
- minder/resources/__init__.py +318 -0
- minder/retrieval/__init__.py +5 -0
- minder/retrieval/hybrid.py +178 -0
- minder/retrieval/mmr.py +116 -0
- minder/retrieval/multi_hop.py +115 -0
- minder/runtime.py +15 -0
- minder/server.py +145 -0
- minder/store/__init__.py +64 -0
- minder/store/document.py +115 -0
- minder/store/error.py +82 -0
- minder/store/feedback.py +114 -0
- minder/store/graph.py +588 -0
- minder/store/history.py +57 -0
- minder/store/interfaces.py +512 -0
- minder/store/milvus/__init__.py +11 -0
- minder/store/milvus/client.py +26 -0
- minder/store/milvus/collections.py +15 -0
- minder/store/milvus/vector_store.py +232 -0
- minder/store/mongodb/__init__.py +11 -0
- minder/store/mongodb/client.py +49 -0
- minder/store/mongodb/indexes.py +90 -0
- minder/store/mongodb/operational_store.py +993 -0
- minder/store/relational.py +1087 -0
- minder/store/repo_state.py +58 -0
- minder/store/rule.py +93 -0
- minder/store/vector.py +79 -0
- minder/tools/__init__.py +47 -0
- minder/tools/auth.py +94 -0
- minder/tools/graph.py +839 -0
- minder/tools/ingest.py +353 -0
- minder/tools/memory.py +381 -0
- minder/tools/query.py +307 -0
- minder/tools/registry.py +269 -0
- minder/tools/repo_scanner.py +1266 -0
- minder/tools/search.py +15 -0
- minder/tools/session.py +316 -0
- minder/tools/skills.py +899 -0
- minder/tools/workflow.py +215 -0
- minder/transport/__init__.py +4 -0
- minder/transport/base.py +286 -0
- minder/transport/sse.py +252 -0
- minder/transport/stdio.py +29 -0
- minder_cli-0.2.0.dist-info/METADATA +318 -0
- minder_cli-0.2.0.dist-info/RECORD +132 -0
- minder_cli-0.2.0.dist-info/WHEEL +4 -0
- minder_cli-0.2.0.dist-info/entry_points.txt +2 -0
- minder_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
minder/auth/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .service import AuthService, AuthError, UserRole, ROLE_HIERARCHY
|
|
2
|
+
from .rbac import require_role
|
|
3
|
+
from .middleware import AuthMiddleware
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AuthService",
|
|
7
|
+
"AuthError",
|
|
8
|
+
"UserRole",
|
|
9
|
+
"ROLE_HIERARCHY",
|
|
10
|
+
"require_role",
|
|
11
|
+
"AuthMiddleware",
|
|
12
|
+
]
|
minder/auth/context.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from minder.auth.principal import Principal
|
|
5
|
+
from minder.models.user import User
|
|
6
|
+
|
|
7
|
+
_current_user: ContextVar[Optional[User]] = ContextVar("current_user", default=None)
|
|
8
|
+
_current_principal: ContextVar[Optional[Principal]] = ContextVar("current_principal", default=None)
|
|
9
|
+
|
|
10
|
+
def set_current_user(user: Optional[User]) -> None:
|
|
11
|
+
_current_user.set(user)
|
|
12
|
+
|
|
13
|
+
def get_current_user() -> Optional[User]:
|
|
14
|
+
return _current_user.get()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_current_principal(principal: Optional[Principal]) -> None:
|
|
18
|
+
_current_principal.set(principal)
|
|
19
|
+
if principal is None or not hasattr(principal, "user"):
|
|
20
|
+
_current_user.set(None)
|
|
21
|
+
else:
|
|
22
|
+
_current_user.set(getattr(principal, "user"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_current_principal() -> Optional[Principal]:
|
|
26
|
+
return _current_principal.get()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auth Middleware — JWT extraction and validation for SSE / HTTP connections.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
middleware = AuthMiddleware(auth_service)
|
|
6
|
+
user = await middleware.authenticate(request.headers.get("Authorization"))
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from minder.auth.principal import Principal
|
|
12
|
+
from minder.auth.service import AuthError, AuthService
|
|
13
|
+
from minder.models.user import User
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthMiddleware:
|
|
17
|
+
"""
|
|
18
|
+
Extracts Bearer JWT from the Authorization header and returns the
|
|
19
|
+
authenticated User. Raises AuthError on any failure so callers can
|
|
20
|
+
convert it to the appropriate HTTP/transport error.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, auth_service: AuthService) -> None:
|
|
24
|
+
self._auth = auth_service
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------------
|
|
27
|
+
# Public API
|
|
28
|
+
# ------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def extract_bearer_token(self, authorization: Optional[str]) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Parse 'Bearer <token>' from the Authorization header value.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
AuthError(AUTH_MISSING_TOKEN) — header absent or empty.
|
|
36
|
+
AuthError(AUTH_INVALID_HEADER) — wrong scheme or malformed.
|
|
37
|
+
"""
|
|
38
|
+
if not authorization or not authorization.strip():
|
|
39
|
+
raise AuthError("AUTH_MISSING_TOKEN", "Authorization header is required")
|
|
40
|
+
|
|
41
|
+
parts = authorization.strip().split(" ", 1)
|
|
42
|
+
if len(parts) != 2 or parts[0].lower() != "bearer":
|
|
43
|
+
raise AuthError(
|
|
44
|
+
"AUTH_INVALID_HEADER",
|
|
45
|
+
"Authorization header must use 'Bearer <token>' scheme",
|
|
46
|
+
)
|
|
47
|
+
token = parts[1].strip()
|
|
48
|
+
if not token:
|
|
49
|
+
raise AuthError("AUTH_MISSING_TOKEN", "Bearer token value is empty")
|
|
50
|
+
return token
|
|
51
|
+
|
|
52
|
+
async def authenticate(self, authorization: Optional[str]) -> User:
|
|
53
|
+
"""
|
|
54
|
+
Full authentication flow: extract token → validate JWT → return user.
|
|
55
|
+
|
|
56
|
+
Raises AuthError on any failure.
|
|
57
|
+
"""
|
|
58
|
+
token = self.extract_bearer_token(authorization)
|
|
59
|
+
return await self._auth.get_user_from_jwt(token)
|
|
60
|
+
|
|
61
|
+
async def authenticate_principal(
|
|
62
|
+
self,
|
|
63
|
+
authorization: Optional[str] = None,
|
|
64
|
+
*,
|
|
65
|
+
client_key: Optional[str] = None,
|
|
66
|
+
) -> Principal:
|
|
67
|
+
if client_key and client_key.strip():
|
|
68
|
+
return await self._auth.get_principal_from_client_key(client_key.strip())
|
|
69
|
+
token = self.extract_bearer_token(authorization)
|
|
70
|
+
return await self._auth.get_principal_from_token(token)
|
minder/auth/principal.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from minder.models.user import User
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class Principal:
|
|
12
|
+
principal_type: str
|
|
13
|
+
principal_id: uuid.UUID
|
|
14
|
+
role: str
|
|
15
|
+
scopes: list[str] = field(default_factory=list)
|
|
16
|
+
repo_scope: list[str] = field(default_factory=list)
|
|
17
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class AdminUserPrincipal(Principal):
|
|
22
|
+
user: User | None = None
|
|
23
|
+
|
|
24
|
+
def __init__(self, user: User) -> None:
|
|
25
|
+
super().__init__(
|
|
26
|
+
principal_type="user",
|
|
27
|
+
principal_id=user.id,
|
|
28
|
+
role=user.role,
|
|
29
|
+
scopes=[],
|
|
30
|
+
repo_scope=[],
|
|
31
|
+
metadata={"email": user.email, "username": user.username},
|
|
32
|
+
)
|
|
33
|
+
self.user = user
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(slots=True)
|
|
37
|
+
class ClientPrincipal(Principal):
|
|
38
|
+
client_id: uuid.UUID = field(init=False)
|
|
39
|
+
client_slug: str = field(init=False)
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
client_id: uuid.UUID,
|
|
45
|
+
client_slug: str,
|
|
46
|
+
scopes: list[str],
|
|
47
|
+
repo_scope: list[str],
|
|
48
|
+
metadata: dict[str, Any] | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
super().__init__(
|
|
51
|
+
principal_type="client",
|
|
52
|
+
principal_id=client_id,
|
|
53
|
+
role="client",
|
|
54
|
+
scopes=scopes,
|
|
55
|
+
repo_scope=repo_scope,
|
|
56
|
+
metadata=metadata or {},
|
|
57
|
+
)
|
|
58
|
+
self.client_id = client_id
|
|
59
|
+
self.client_slug = client_slug
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from minder.auth.principal import Principal
|
|
7
|
+
from minder.auth.service import AuthError
|
|
8
|
+
from minder.config import MinderConfig
|
|
9
|
+
from minder.store.interfaces import ICacheProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RateLimitError(AuthError):
|
|
13
|
+
def __init__(self, message: str) -> None:
|
|
14
|
+
super().__init__("AUTH_RATE_LIMITED", message)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class RateLimitDecision:
|
|
19
|
+
limit: int
|
|
20
|
+
count: int
|
|
21
|
+
remaining: int
|
|
22
|
+
reset_at: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RateLimiter:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
cache: ICacheProvider,
|
|
30
|
+
config: MinderConfig,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._cache = cache
|
|
33
|
+
self._config = config
|
|
34
|
+
|
|
35
|
+
def enabled(self) -> bool:
|
|
36
|
+
return self._config.rate_limit.enabled
|
|
37
|
+
|
|
38
|
+
def _limit_for_principal(self, principal: Principal) -> int:
|
|
39
|
+
if principal.principal_type == "client":
|
|
40
|
+
return self._config.rate_limit.client_limit
|
|
41
|
+
role = principal.role
|
|
42
|
+
if role == "admin":
|
|
43
|
+
return self._config.rate_limit.admin_limit
|
|
44
|
+
if role == "readonly":
|
|
45
|
+
return self._config.rate_limit.readonly_limit
|
|
46
|
+
return self._config.rate_limit.member_limit
|
|
47
|
+
|
|
48
|
+
def _key(self, principal: Principal, tool_name: str, window_start: int) -> str:
|
|
49
|
+
return f"rate_limit:{principal.principal_type}:{principal.principal_id}:{tool_name}:{window_start}"
|
|
50
|
+
|
|
51
|
+
async def enforce(self, *, principal: Principal, tool_name: str) -> RateLimitDecision:
|
|
52
|
+
limit = self._limit_for_principal(principal)
|
|
53
|
+
now = int(time.time())
|
|
54
|
+
window = self._config.rate_limit.window_seconds
|
|
55
|
+
window_start = now - (now % window)
|
|
56
|
+
reset_at = window_start + window
|
|
57
|
+
key = self._key(principal, tool_name, window_start)
|
|
58
|
+
|
|
59
|
+
count = await self._cache.incr(key)
|
|
60
|
+
if count == 1:
|
|
61
|
+
await self._cache.expire(key, window)
|
|
62
|
+
|
|
63
|
+
if count > limit:
|
|
64
|
+
raise RateLimitError(
|
|
65
|
+
f"Rate limit exceeded for {principal.principal_type} '{principal.principal_id}' on tool '{tool_name}'"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return RateLimitDecision(
|
|
69
|
+
limit=limit,
|
|
70
|
+
count=count,
|
|
71
|
+
remaining=max(limit - count, 0),
|
|
72
|
+
reset_at=reset_at,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
async def get_usage(self, *, principal: Principal, tool_name: str) -> dict[str, int]:
|
|
76
|
+
limit = self._limit_for_principal(principal)
|
|
77
|
+
now = int(time.time())
|
|
78
|
+
window = self._config.rate_limit.window_seconds
|
|
79
|
+
window_start = now - (now % window)
|
|
80
|
+
reset_at = window_start + window
|
|
81
|
+
key = self._key(principal, tool_name, window_start)
|
|
82
|
+
raw = await self._cache.get(key)
|
|
83
|
+
count = int(raw or "0")
|
|
84
|
+
return {
|
|
85
|
+
"count": count,
|
|
86
|
+
"limit": limit,
|
|
87
|
+
"remaining": max(limit - count, 0),
|
|
88
|
+
"reset_at": reset_at,
|
|
89
|
+
}
|
minder/auth/rbac.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RBAC — role-based access control decorator.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
@require_role(UserRole.ADMIN)
|
|
6
|
+
async def admin_only_handler(*args, user: User, **kwargs):
|
|
7
|
+
...
|
|
8
|
+
|
|
9
|
+
The decorated function must receive the authenticated `user` as a keyword arg.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import functools
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
from minder.auth.service import AuthError, ROLE_HIERARCHY, UserRole
|
|
16
|
+
from minder.models.user import User
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def require_role(min_role: UserRole) -> Callable:
|
|
20
|
+
"""
|
|
21
|
+
Decorator that enforces a minimum role on an async handler.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
AuthError(AUTH_NO_USER) — if `user` kwarg is absent.
|
|
25
|
+
AuthError(AUTH_INSUFFICIENT_ROLE) — if user's role is below min_role.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def decorator(func: Callable) -> Callable:
|
|
29
|
+
@functools.wraps(func)
|
|
30
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
31
|
+
user: User | None = kwargs.get("user")
|
|
32
|
+
if user is None:
|
|
33
|
+
raise AuthError(
|
|
34
|
+
"AUTH_NO_USER",
|
|
35
|
+
"No authenticated user provided to role-guarded handler",
|
|
36
|
+
)
|
|
37
|
+
try:
|
|
38
|
+
user_role = UserRole(user.role)
|
|
39
|
+
except ValueError:
|
|
40
|
+
raise AuthError(
|
|
41
|
+
"AUTH_UNKNOWN_ROLE",
|
|
42
|
+
f"Unrecognised role value: {user.role!r}",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
user_level = ROLE_HIERARCHY.get(user_role, 0)
|
|
46
|
+
required_level = ROLE_HIERARCHY.get(min_role, 0)
|
|
47
|
+
|
|
48
|
+
if user_level < required_level:
|
|
49
|
+
raise AuthError(
|
|
50
|
+
"AUTH_INSUFFICIENT_ROLE",
|
|
51
|
+
(
|
|
52
|
+
f"Role '{user_role.value}' is insufficient. "
|
|
53
|
+
f"Required: '{min_role.value}' or higher."
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
return await func(*args, **kwargs)
|
|
57
|
+
|
|
58
|
+
return wrapper
|
|
59
|
+
|
|
60
|
+
return decorator
|