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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime, UTC
|
|
3
|
+
from typing import Dict, Any, List, Optional
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
5
|
+
from sqlalchemy import String, DateTime, UUID, JSON, func
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from .base import Base, BaseModelMeta
|
|
9
|
+
|
|
10
|
+
class RepositorySchema(BaseModelMeta):
|
|
11
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
12
|
+
repo_name: str
|
|
13
|
+
repo_url: str
|
|
14
|
+
default_branch: str
|
|
15
|
+
tracked_branches: List[str] = Field(default_factory=list)
|
|
16
|
+
workflow_id: Optional[uuid.UUID] = None
|
|
17
|
+
state_path: str = ".minder"
|
|
18
|
+
context_snapshot: Dict[str, Any] = Field(default_factory=dict)
|
|
19
|
+
relationships: Dict[str, Any] = Field(default_factory=dict)
|
|
20
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
21
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
22
|
+
|
|
23
|
+
class Repository(Base):
|
|
24
|
+
__tablename__ = "repositories"
|
|
25
|
+
|
|
26
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
27
|
+
company_id: Mapped[str] = mapped_column(String, index=True, default="default")
|
|
28
|
+
repo_name: Mapped[str] = mapped_column(String, index=True)
|
|
29
|
+
repo_url: Mapped[str] = mapped_column(String)
|
|
30
|
+
default_branch: Mapped[str] = mapped_column(String)
|
|
31
|
+
# v2: list of branches that have been synced/tracked (stored as JSON array)
|
|
32
|
+
tracked_branches: Mapped[List[str]] = mapped_column(JSON, default=list, nullable=True)
|
|
33
|
+
workflow_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True)
|
|
34
|
+
state_path: Mapped[str] = mapped_column(String, default=".minder")
|
|
35
|
+
context_snapshot: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
|
|
36
|
+
relationships: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
|
|
37
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
38
|
+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
39
|
+
|
|
40
|
+
class RepositoryWorkflowStateSchema(BaseModelMeta):
|
|
41
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
42
|
+
repo_id: uuid.UUID
|
|
43
|
+
session_id: Optional[uuid.UUID] = None
|
|
44
|
+
current_step: str
|
|
45
|
+
completed_steps: List[str] = Field(default_factory=list)
|
|
46
|
+
blocked_by: List[str] = Field(default_factory=list)
|
|
47
|
+
artifacts: Dict[str, Any] = Field(default_factory=dict)
|
|
48
|
+
next_step: Optional[str] = None
|
|
49
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
50
|
+
|
|
51
|
+
class RepositoryWorkflowState(Base):
|
|
52
|
+
__tablename__ = "repository_workflow_states"
|
|
53
|
+
|
|
54
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
55
|
+
repo_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
|
|
56
|
+
session_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True, index=True)
|
|
57
|
+
current_step: Mapped[str] = mapped_column(String)
|
|
58
|
+
completed_steps: Mapped[Dict[str, Any]] = mapped_column(JSON, default=list) # stored as JSON list
|
|
59
|
+
blocked_by: Mapped[Dict[str, Any]] = mapped_column(JSON, default=list) # stored as JSON list
|
|
60
|
+
artifacts: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
|
|
61
|
+
next_step: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
62
|
+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
minder/models/rule.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime, UTC
|
|
3
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
4
|
+
from sqlalchemy import JSON, String, Boolean, Integer, DateTime, UUID, func
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from .base import Base, BaseModelMeta
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RuleSchema(BaseModelMeta):
|
|
11
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
12
|
+
title: str
|
|
13
|
+
description: str
|
|
14
|
+
pattern: str
|
|
15
|
+
content: str
|
|
16
|
+
priority: int = 0
|
|
17
|
+
scope: str # enum: global, project, language, repository
|
|
18
|
+
active: bool = True
|
|
19
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Rule(Base):
|
|
23
|
+
__tablename__ = "rules"
|
|
24
|
+
|
|
25
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
26
|
+
title: Mapped[str] = mapped_column(String)
|
|
27
|
+
description: Mapped[str] = mapped_column(String)
|
|
28
|
+
pattern: Mapped[str] = mapped_column(String)
|
|
29
|
+
content: Mapped[str] = mapped_column(String)
|
|
30
|
+
priority: Mapped[int] = mapped_column(Integer, default=0)
|
|
31
|
+
scope: Mapped[str] = mapped_column(String, index=True)
|
|
32
|
+
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
33
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FeedbackSchema(BaseModelMeta):
|
|
37
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
38
|
+
entity_type: str # enum: skill, response, retrieval, workflow
|
|
39
|
+
entity_id: uuid.UUID
|
|
40
|
+
rating: int # 1 to 5
|
|
41
|
+
feedback_text: str = ""
|
|
42
|
+
context: dict = Field(default_factory=dict)
|
|
43
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Feedback(Base):
|
|
47
|
+
"""SQLAlchemy ORM model for user/system feedback on entities."""
|
|
48
|
+
|
|
49
|
+
__tablename__ = "feedback"
|
|
50
|
+
|
|
51
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
52
|
+
entity_type: Mapped[str] = mapped_column(String, index=True)
|
|
53
|
+
entity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
|
|
54
|
+
rating: Mapped[int] = mapped_column(Integer)
|
|
55
|
+
feedback_text: Mapped[str] = mapped_column(String, default="")
|
|
56
|
+
context: Mapped[dict] = mapped_column(JSON, default=dict)
|
|
57
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MetadataSchema(BaseModelMeta):
|
|
61
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
62
|
+
entity_type: str # enum: skill, history, error, document, workflow
|
|
63
|
+
entity_id: uuid.UUID
|
|
64
|
+
key: str
|
|
65
|
+
value: dict = Field(default_factory=dict)
|
|
66
|
+
source: str # enum: user, system, import
|
|
67
|
+
version: int = 1
|
|
68
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
minder/models/session.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime, UTC
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
5
|
+
from sqlalchemy import String, Integer, DateTime, UUID, JSON, func
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from .base import Base, BaseModelMeta
|
|
9
|
+
|
|
10
|
+
class SessionSchema(BaseModelMeta):
|
|
11
|
+
"""Pydantic schema for session serialisation / validation."""
|
|
12
|
+
|
|
13
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
14
|
+
# Owner — exactly one of user_id or client_id is set.
|
|
15
|
+
user_id: Optional[uuid.UUID] = None
|
|
16
|
+
client_id: Optional[uuid.UUID] = None
|
|
17
|
+
# Human-readable project label for cross-environment lookup.
|
|
18
|
+
name: Optional[str] = None
|
|
19
|
+
repo_id: Optional[uuid.UUID] = None
|
|
20
|
+
project_context: Dict[str, Any] = Field(default_factory=dict)
|
|
21
|
+
active_skills: Dict[str, Any] = Field(default_factory=dict)
|
|
22
|
+
state: Dict[str, Any] = Field(default_factory=dict)
|
|
23
|
+
ttl: int = 86400 # 24 h default — long enough for multi-day work continuity
|
|
24
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
25
|
+
last_active: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Session(Base):
|
|
29
|
+
"""SQLAlchemy ORM model for the ``sessions`` table.
|
|
30
|
+
|
|
31
|
+
Production store: MongoDB (``src/minder/store/mongodb/operational_store.py``).
|
|
32
|
+
This SQLAlchemy model is retained for unit-test fixtures (SQLite in-memory)
|
|
33
|
+
and is created fresh via ``Base.metadata.create_all`` — no migration needed.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
__tablename__ = "sessions"
|
|
37
|
+
|
|
38
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
39
|
+
company_id: Mapped[str] = mapped_column(String, index=True, default="default")
|
|
40
|
+
# Owner columns — mutually exclusive, both nullable.
|
|
41
|
+
user_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True, index=True)
|
|
42
|
+
client_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True, index=True)
|
|
43
|
+
# Optional project label — enables cross-environment lookup by name.
|
|
44
|
+
name: Mapped[Optional[str]] = mapped_column(String, nullable=True, index=True)
|
|
45
|
+
repo_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True, index=True)
|
|
46
|
+
project_context: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
|
|
47
|
+
active_skills: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
|
|
48
|
+
state: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
|
|
49
|
+
ttl: Mapped[int] = mapped_column(Integer, default=86400)
|
|
50
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
51
|
+
last_active: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
minder/models/skill.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime, UTC
|
|
3
|
+
from typing import Dict, Any, List, Optional
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
5
|
+
from sqlalchemy import String, Integer, Float, DateTime, UUID, JSON, func
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from .base import Base, BaseModelMeta
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Pydantic Schema
|
|
12
|
+
class SkillSchema(BaseModelMeta):
|
|
13
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
14
|
+
title: str
|
|
15
|
+
content: str
|
|
16
|
+
language: str
|
|
17
|
+
tags: List[str] = Field(default_factory=list)
|
|
18
|
+
embedding: Optional[List[float]] = None # vector(default 768) stored as JSON list
|
|
19
|
+
usage_count: int = 0
|
|
20
|
+
quality_score: float = 0.0
|
|
21
|
+
source_metadata: Optional[Dict[str, Any]] = None
|
|
22
|
+
excerpt_kind: str = "none"
|
|
23
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
24
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# SQLAlchemy Model
|
|
28
|
+
class Skill(Base):
|
|
29
|
+
__tablename__ = "skills"
|
|
30
|
+
|
|
31
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
32
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
33
|
+
)
|
|
34
|
+
company_id: Mapped[str] = mapped_column(String, index=True, default="default")
|
|
35
|
+
title: Mapped[str] = mapped_column(String, index=True)
|
|
36
|
+
content: Mapped[str] = mapped_column(String)
|
|
37
|
+
language: Mapped[str] = mapped_column(String, index=True)
|
|
38
|
+
tags: Mapped[Dict[str, Any]] = mapped_column(JSON, default=list)
|
|
39
|
+
# Embedding stored as JSON list for cross-dialect compatibility (SQLite dev / PostgreSQL prod)
|
|
40
|
+
embedding: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True)
|
|
41
|
+
usage_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
42
|
+
quality_score: Mapped[float] = mapped_column(Float, default=0.0)
|
|
43
|
+
source_metadata: Mapped[Optional[Dict[str, Any]]] = mapped_column(
|
|
44
|
+
JSON, nullable=True
|
|
45
|
+
)
|
|
46
|
+
excerpt_kind: Mapped[str] = mapped_column(String, default="none")
|
|
47
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
48
|
+
DateTime(timezone=True), server_default=func.now()
|
|
49
|
+
)
|
|
50
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
51
|
+
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
|
52
|
+
)
|
minder/models/user.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime, UTC
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
5
|
+
from sqlalchemy import String, Boolean, DateTime, UUID, JSON, func
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from .base import Base, BaseModelMeta
|
|
9
|
+
|
|
10
|
+
# Pydantic Schema
|
|
11
|
+
class UserSchema(BaseModelMeta):
|
|
12
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
13
|
+
email: str
|
|
14
|
+
username: str
|
|
15
|
+
display_name: str
|
|
16
|
+
api_key_hash: str
|
|
17
|
+
# bcrypt/pbkdf2 hash of the login password; None means password login disabled
|
|
18
|
+
password_hash: Optional[str] = None
|
|
19
|
+
role: str
|
|
20
|
+
settings: Dict[str, Any] = Field(default_factory=dict)
|
|
21
|
+
is_active: bool = True
|
|
22
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
23
|
+
last_login: Optional[datetime] = None
|
|
24
|
+
|
|
25
|
+
# SQLAlchemy Model
|
|
26
|
+
class User(Base):
|
|
27
|
+
__tablename__ = "users"
|
|
28
|
+
|
|
29
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
30
|
+
company_id: Mapped[str] = mapped_column(String, index=True, default="default")
|
|
31
|
+
email: Mapped[str] = mapped_column(String, unique=True, index=True)
|
|
32
|
+
username: Mapped[str] = mapped_column(String, unique=True, index=True)
|
|
33
|
+
display_name: Mapped[str] = mapped_column(String)
|
|
34
|
+
api_key_hash: Mapped[str] = mapped_column(String)
|
|
35
|
+
# Optional bcrypt/pbkdf2 hash — null means only API-key auth available
|
|
36
|
+
password_hash: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
37
|
+
role: Mapped[str] = mapped_column(String)
|
|
38
|
+
settings: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
|
|
39
|
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
40
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
41
|
+
last_login: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime, UTC
|
|
3
|
+
from typing import Dict, Any, List
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
5
|
+
from sqlalchemy import String, Boolean, Integer, DateTime, UUID, JSON, func
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from .base import Base, BaseModelMeta
|
|
9
|
+
|
|
10
|
+
class WorkflowSchema(BaseModelMeta):
|
|
11
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
12
|
+
name: str
|
|
13
|
+
description: str = ""
|
|
14
|
+
enforcement: str = "strict"
|
|
15
|
+
version: int = 1
|
|
16
|
+
steps: List[Dict[str, Any]] = Field(default_factory=list)
|
|
17
|
+
policies: Dict[str, Any] = Field(default_factory=dict)
|
|
18
|
+
default_for_repo: bool = False
|
|
19
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
20
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
21
|
+
|
|
22
|
+
class Workflow(Base):
|
|
23
|
+
__tablename__ = "workflows"
|
|
24
|
+
|
|
25
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
26
|
+
company_id: Mapped[str] = mapped_column(String, index=True, default="default")
|
|
27
|
+
name: Mapped[str] = mapped_column(String, index=True)
|
|
28
|
+
description: Mapped[str] = mapped_column(String, default="")
|
|
29
|
+
enforcement: Mapped[str] = mapped_column(String, default="strict")
|
|
30
|
+
version: Mapped[int] = mapped_column(Integer, default=1)
|
|
31
|
+
steps: Mapped[Dict[str, Any]] = mapped_column(JSON, default=list) # list of step dicts
|
|
32
|
+
policies: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
|
|
33
|
+
default_for_repo: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
34
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
35
|
+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Observability package for Minder.
|
|
2
|
+
|
|
3
|
+
Public re-exports used throughout the application:
|
|
4
|
+
|
|
5
|
+
from minder.observability import (
|
|
6
|
+
configure_logging,
|
|
7
|
+
configure_tracing,
|
|
8
|
+
get_tracer,
|
|
9
|
+
trace_async,
|
|
10
|
+
record_tool_call,
|
|
11
|
+
record_auth_event,
|
|
12
|
+
record_http_request,
|
|
13
|
+
AuditEmitter,
|
|
14
|
+
metrics_endpoint,
|
|
15
|
+
CorrelationIdMiddleware,
|
|
16
|
+
AccessLogMiddleware,
|
|
17
|
+
)
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from minder.observability.audit import AuditEmitter
|
|
22
|
+
from minder.observability.logging import (
|
|
23
|
+
AccessLogMiddleware,
|
|
24
|
+
CorrelationIdMiddleware,
|
|
25
|
+
JsonFormatter,
|
|
26
|
+
configure_json_logging,
|
|
27
|
+
get_correlation_id,
|
|
28
|
+
)
|
|
29
|
+
from minder.observability.metrics import (
|
|
30
|
+
metrics_endpoint,
|
|
31
|
+
record_admin_operation,
|
|
32
|
+
record_auth_event,
|
|
33
|
+
record_http_request,
|
|
34
|
+
record_tool_call,
|
|
35
|
+
)
|
|
36
|
+
from minder.observability.tracing import configure_tracing, get_tracer, trace_async
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# audit
|
|
40
|
+
"AuditEmitter",
|
|
41
|
+
# logging
|
|
42
|
+
"AccessLogMiddleware",
|
|
43
|
+
"CorrelationIdMiddleware",
|
|
44
|
+
"JsonFormatter",
|
|
45
|
+
"configure_json_logging",
|
|
46
|
+
"get_correlation_id",
|
|
47
|
+
# metrics
|
|
48
|
+
"metrics_endpoint",
|
|
49
|
+
"record_admin_operation",
|
|
50
|
+
"record_auth_event",
|
|
51
|
+
"record_http_request",
|
|
52
|
+
"record_tool_call",
|
|
53
|
+
# tracing
|
|
54
|
+
"configure_tracing",
|
|
55
|
+
"get_tracer",
|
|
56
|
+
"trace_async",
|
|
57
|
+
]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Durable audit event emitter for Minder.
|
|
2
|
+
|
|
3
|
+
Wraps ``store.create_audit_log()`` with a higher-level convenience API and
|
|
4
|
+
also emits a structured log entry for every event so that operators have
|
|
5
|
+
both persistent audit records and real-time log streams.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
emitter = AuditEmitter(store=store)
|
|
10
|
+
|
|
11
|
+
await emitter.emit(
|
|
12
|
+
actor_type="admin",
|
|
13
|
+
actor_id=str(admin.id),
|
|
14
|
+
event_type="client.created",
|
|
15
|
+
resource_type="client",
|
|
16
|
+
resource_id=str(client.id),
|
|
17
|
+
outcome="success",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Convenience helpers
|
|
21
|
+
await emitter.client_created(actor_id=..., client_id=..., metadata={...})
|
|
22
|
+
await emitter.key_rotated(actor_id=..., client_id=...)
|
|
23
|
+
await emitter.auth_login(actor_id=..., outcome="success")
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import uuid
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from minder.store.interfaces import IOperationalStore
|
|
32
|
+
|
|
33
|
+
_log = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuditEmitter:
|
|
37
|
+
"""Emit structured audit events to the operational store and to the log.
|
|
38
|
+
|
|
39
|
+
An ``AuditEmitter`` is safe to construct once at application start and
|
|
40
|
+
reused across requests. All methods are coroutines because the store
|
|
41
|
+
write is async.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, store: IOperationalStore) -> None:
|
|
45
|
+
self._store = store
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# Core emit
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
async def emit(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
actor_type: str,
|
|
55
|
+
actor_id: str,
|
|
56
|
+
event_type: str,
|
|
57
|
+
resource_type: str,
|
|
58
|
+
resource_id: str,
|
|
59
|
+
outcome: str = "success",
|
|
60
|
+
metadata: dict[str, Any] | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Persist one audit event and write it to the structured log.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
actor_type: Who performed the action (``"admin"`` | ``"client"`` | ``"system"``).
|
|
66
|
+
actor_id: UUID string of the actor.
|
|
67
|
+
event_type: Dot-separated action label (``"client.created"``, ``"key.rotated"``…).
|
|
68
|
+
resource_type: Type of the affected resource (``"client"`` | ``"key"`` | …).
|
|
69
|
+
resource_id: UUID string of the affected resource.
|
|
70
|
+
outcome: ``"success"`` | ``"failure"`` | ``"denied"``.
|
|
71
|
+
metadata: Optional extra structured data persisted with the event.
|
|
72
|
+
"""
|
|
73
|
+
audit_metadata = metadata or {}
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
await self._store.create_audit_log(
|
|
77
|
+
id=str(uuid.uuid4()),
|
|
78
|
+
actor_type=actor_type,
|
|
79
|
+
actor_id=actor_id,
|
|
80
|
+
event_type=event_type,
|
|
81
|
+
resource_type=resource_type,
|
|
82
|
+
resource_id=resource_id,
|
|
83
|
+
outcome=outcome,
|
|
84
|
+
audit_metadata=audit_metadata,
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
_log.exception(
|
|
88
|
+
"Failed to persist audit event",
|
|
89
|
+
extra={
|
|
90
|
+
"audit_event_type": event_type,
|
|
91
|
+
"audit_actor_id": actor_id,
|
|
92
|
+
"audit_resource_id": resource_id,
|
|
93
|
+
"audit_outcome": outcome,
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
_log.info(
|
|
98
|
+
"audit: %s %s → %s",
|
|
99
|
+
actor_type,
|
|
100
|
+
event_type,
|
|
101
|
+
outcome,
|
|
102
|
+
extra={
|
|
103
|
+
"audit_actor_type": actor_type,
|
|
104
|
+
"audit_actor_id": actor_id,
|
|
105
|
+
"audit_event_type": event_type,
|
|
106
|
+
"audit_resource_type": resource_type,
|
|
107
|
+
"audit_resource_id": resource_id,
|
|
108
|
+
"audit_outcome": outcome,
|
|
109
|
+
**audit_metadata,
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
# Convenience helpers — auth/lifecycle events
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
async def auth_login(
|
|
118
|
+
self,
|
|
119
|
+
*,
|
|
120
|
+
actor_id: str,
|
|
121
|
+
actor_type: str = "admin",
|
|
122
|
+
outcome: str = "success",
|
|
123
|
+
metadata: dict[str, Any] | None = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
await self.emit(
|
|
126
|
+
actor_type=actor_type,
|
|
127
|
+
actor_id=actor_id,
|
|
128
|
+
event_type="auth.login",
|
|
129
|
+
resource_type="session",
|
|
130
|
+
resource_id=actor_id,
|
|
131
|
+
outcome=outcome,
|
|
132
|
+
metadata=metadata,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def auth_logout(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
actor_id: str,
|
|
139
|
+
actor_type: str = "admin",
|
|
140
|
+
) -> None:
|
|
141
|
+
await self.emit(
|
|
142
|
+
actor_type=actor_type,
|
|
143
|
+
actor_id=actor_id,
|
|
144
|
+
event_type="auth.logout",
|
|
145
|
+
resource_type="session",
|
|
146
|
+
resource_id=actor_id,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def client_created(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
actor_id: str,
|
|
153
|
+
client_id: str,
|
|
154
|
+
metadata: dict[str, Any] | None = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
await self.emit(
|
|
157
|
+
actor_type="admin",
|
|
158
|
+
actor_id=actor_id,
|
|
159
|
+
event_type="client.created",
|
|
160
|
+
resource_type="client",
|
|
161
|
+
resource_id=client_id,
|
|
162
|
+
metadata=metadata,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def client_updated(
|
|
166
|
+
self,
|
|
167
|
+
*,
|
|
168
|
+
actor_id: str,
|
|
169
|
+
client_id: str,
|
|
170
|
+
fields_changed: list[str] | None = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
await self.emit(
|
|
173
|
+
actor_type="admin",
|
|
174
|
+
actor_id=actor_id,
|
|
175
|
+
event_type="client.updated",
|
|
176
|
+
resource_type="client",
|
|
177
|
+
resource_id=client_id,
|
|
178
|
+
metadata={"fields_changed": fields_changed or []},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async def key_rotated(
|
|
182
|
+
self,
|
|
183
|
+
*,
|
|
184
|
+
actor_id: str,
|
|
185
|
+
client_id: str,
|
|
186
|
+
) -> None:
|
|
187
|
+
await self.emit(
|
|
188
|
+
actor_type="admin",
|
|
189
|
+
actor_id=actor_id,
|
|
190
|
+
event_type="key.rotated",
|
|
191
|
+
resource_type="client",
|
|
192
|
+
resource_id=client_id,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
async def key_revoked(
|
|
196
|
+
self,
|
|
197
|
+
*,
|
|
198
|
+
actor_id: str,
|
|
199
|
+
client_id: str,
|
|
200
|
+
) -> None:
|
|
201
|
+
await self.emit(
|
|
202
|
+
actor_type="admin",
|
|
203
|
+
actor_id=actor_id,
|
|
204
|
+
event_type="key.revoked",
|
|
205
|
+
resource_type="client",
|
|
206
|
+
resource_id=client_id,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def token_exchanged(
|
|
210
|
+
self,
|
|
211
|
+
*,
|
|
212
|
+
actor_id: str,
|
|
213
|
+
client_id: str,
|
|
214
|
+
scopes: list[str] | None = None,
|
|
215
|
+
outcome: str = "success",
|
|
216
|
+
) -> None:
|
|
217
|
+
await self.emit(
|
|
218
|
+
actor_type="client",
|
|
219
|
+
actor_id=actor_id,
|
|
220
|
+
event_type="token.exchanged",
|
|
221
|
+
resource_type="client",
|
|
222
|
+
resource_id=client_id,
|
|
223
|
+
outcome=outcome,
|
|
224
|
+
metadata={"scopes": scopes or []},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def tool_call(
|
|
228
|
+
self,
|
|
229
|
+
*,
|
|
230
|
+
actor_id: str,
|
|
231
|
+
tool_name: str,
|
|
232
|
+
outcome: str = "success",
|
|
233
|
+
metadata: dict[str, Any] | None = None,
|
|
234
|
+
) -> None:
|
|
235
|
+
await self.emit(
|
|
236
|
+
actor_type="client",
|
|
237
|
+
actor_id=actor_id,
|
|
238
|
+
event_type=f"tool.{tool_name}",
|
|
239
|
+
resource_type="tool",
|
|
240
|
+
resource_id=tool_name,
|
|
241
|
+
outcome=outcome,
|
|
242
|
+
metadata=metadata,
|
|
243
|
+
)
|