mesa-memory 0.9.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.
- mesa_api/__init__.py +9 -0
- mesa_api/admission.py +23 -0
- mesa_api/identifier_validation.py +67 -0
- mesa_api/routers/control/router.py +276 -0
- mesa_api/schemas.py +16 -0
- mesa_api/v4_router.py +1624 -0
- mesa_client/__init__.py +27 -0
- mesa_client/client.py +1265 -0
- mesa_mcp/__init__.py +5 -0
- mesa_mcp/adapter.py +226 -0
- mesa_mcp/antigravity_bridge.py +466 -0
- mesa_mcp/antigravity_cli.py +334 -0
- mesa_mcp/bounded_cache.py +133 -0
- mesa_mcp/codex_cli.py +631 -0
- mesa_mcp/codex_hooks.py +161 -0
- mesa_mcp/configuration.py +100 -0
- mesa_mcp/errors.py +24 -0
- mesa_mcp/gateway/__init__.py +1 -0
- mesa_mcp/gateway/anomaly.py +30 -0
- mesa_mcp/gateway/app.py +270 -0
- mesa_mcp/gateway/approval.py +132 -0
- mesa_mcp/gateway/auth.py +32 -0
- mesa_mcp/gateway/codex_transport.py +148 -0
- mesa_mcp/gateway/middleware.py +182 -0
- mesa_mcp/gateway/operations.py +955 -0
- mesa_mcp/gateway/policy/__init__.py +1 -0
- mesa_mcp/gateway/policy/engine.py +80 -0
- mesa_mcp/gateway/policy/simulator.py +45 -0
- mesa_mcp/http_service.py +63 -0
- mesa_mcp/local_cli.py +374 -0
- mesa_mcp/security.py +68 -0
- mesa_mcp/server.py +317 -0
- mesa_mcp/service.py +25 -0
- mesa_mcp/v4_service.py +582 -0
- mesa_mcp/workspace.py +23 -0
- mesa_memory/adapter/base.py +49 -0
- mesa_memory/adapter/claude.py +220 -0
- mesa_memory/adapter/data/cl100k_base.tiktoken +100256 -0
- mesa_memory/adapter/endpoints.py +14 -0
- mesa_memory/adapter/factory.py +288 -0
- mesa_memory/adapter/live.py +274 -0
- mesa_memory/adapter/ollama.py +113 -0
- mesa_memory/adapter/tokenizer.py +98 -0
- mesa_memory/api/middleware.py +91 -0
- mesa_memory/api/server.py +926 -0
- mesa_memory/config.py +993 -0
- mesa_memory/consolidation/loop.py +1179 -0
- mesa_memory/consolidation/parser.py +271 -0
- mesa_memory/consolidation/policy.py +173 -0
- mesa_memory/consolidation/router.py +392 -0
- mesa_memory/consolidation/schemas.py +255 -0
- mesa_memory/consolidation/validator.py +286 -0
- mesa_memory/container_health.py +25 -0
- mesa_memory/context_builder.py +693 -0
- mesa_memory/document_chunking.py +99 -0
- mesa_memory/embedding/__init__.py +21 -0
- mesa_memory/embedding/service.py +752 -0
- mesa_memory/extraction/service.py +614 -0
- mesa_memory/graph/__init__.py +5 -0
- mesa_memory/graph/projector.py +43 -0
- mesa_memory/observability/http.py +122 -0
- mesa_memory/observability/logger.py +259 -0
- mesa_memory/observability/metrics.py +252 -0
- mesa_memory/rebuild_runner.py +505 -0
- mesa_memory/retrieval/adaptive.py +311 -0
- mesa_memory/retrieval/core.py +2 -0
- mesa_memory/retrieval/legal_resolver.py +15 -0
- mesa_memory/runtime_entrypoint.py +49 -0
- mesa_memory/security/admin_cli.py +204 -0
- mesa_memory/security/api_keys.py +267 -0
- mesa_memory/security/input_validation.py +50 -0
- mesa_memory/security/rbac.py +787 -0
- mesa_memory/security/rbac_constants.py +25 -0
- mesa_memory/security/untrusted_memory.py +45 -0
- mesa_memory/utils.py +36 -0
- mesa_memory-0.9.0.dist-info/METADATA +213 -0
- mesa_memory-0.9.0.dist-info/RECORD +157 -0
- mesa_memory-0.9.0.dist-info/WHEEL +5 -0
- mesa_memory-0.9.0.dist-info/entry_points.txt +8 -0
- mesa_memory-0.9.0.dist-info/licenses/LICENSE +21 -0
- mesa_memory-0.9.0.dist-info/top_level.txt +6 -0
- mesa_storage/__init__.py +42 -0
- mesa_storage/alembic/README +1 -0
- mesa_storage/alembic/env.py +133 -0
- mesa_storage/alembic/script.py.mako +28 -0
- mesa_storage/alembic/versions/076eef5d1b6c_add_daily_limits.py +34 -0
- mesa_storage/alembic/versions/087de6628c51_add_mcp_activity_approval.py +81 -0
- mesa_storage/alembic/versions/0a7b8c9d0e1f_scope_catalog_physical_ids.py +42 -0
- mesa_storage/alembic/versions/1e7a061f7f9e_add_mcp_clients_and_bindings.py +61 -0
- mesa_storage/alembic/versions/41402f316580_add_temporal_validity_to_nodes.py +30 -0
- mesa_storage/alembic/versions/4933fb5fd0ea_initial_schema.py +143 -0
- mesa_storage/alembic/versions/9a1b2c3d4e5f_add_v4_catalog_provenance.py +367 -0
- mesa_storage/alembic/versions/9c6c7ae69ed7_add_mcp_policy_rules.py +41 -0
- mesa_storage/alembic/versions/a1d2e3f4b5c6_add_wal_projection_states.py +53 -0
- mesa_storage/alembic/versions/a2b3c4d5e6f7_add_declared_content_hash.py +24 -0
- mesa_storage/alembic/versions/a94df5d14fce_add_connections_and_settings.py +55 -0
- mesa_storage/alembic/versions/b2e3f4a5c6d7_add_session_finalization_journal.py +32 -0
- mesa_storage/alembic/versions/b3c4d5e6f7a8_allow_unknown_revision_content_hash.py +29 -0
- mesa_storage/alembic/versions/bb2355d0cdd4_add_epistemic_columns.py +38 -0
- mesa_storage/alembic/versions/c3d4e5f6a7b8_replace_daily_limit_credential_subject.py +45 -0
- mesa_storage/alembic/versions/c4d5e6f7a8b9_add_assertion_fts_and_representation.py +119 -0
- mesa_storage/alembic/versions/c4f1a8e2d9b0_add_purge_journal.py +62 -0
- mesa_storage/alembic/versions/c5d6e7f8a9b0_add_mcp_operation_ledger.py +58 -0
- mesa_storage/alembic/versions/d4e5f6a7b8c9_add_v4_mutation_ledger.py +96 -0
- mesa_storage/alembic/versions/d5e6f7a8b9c0_add_typed_legal_identity.py +65 -0
- mesa_storage/alembic/versions/d6e7f8a9b0c1_add_mcp_client_credentials.py +35 -0
- mesa_storage/alembic/versions/e7f8a9b0c1d2_add_mcp_codex_profiles.py +33 -0
- mesa_storage/alembic/versions/e9b7c3a1d4f2_add_claim_leases.py +61 -0
- mesa_storage/alembic/versions/f6d4a7b8c9e0_add_dispatch_journal.py +48 -0
- mesa_storage/alembic/versions/f7e5b9c0d1a2_add_dispatch_queue_payload_bytes.py +33 -0
- mesa_storage/alembic/versions/f8a6c0d1e2b3_add_dispatch_completion_receipts.py +44 -0
- mesa_storage/alembic/versions/f8a9b0c1d2e3_generalize_mcp_context_profiles.py +41 -0
- mesa_storage/alembic/versions/f9a0b1c2d3e4_enforce_catalog_scope_integrity.py +44 -0
- mesa_storage/alembic/versions/fa1b2c3d4e5f_add_mcp_credential_expiry.py +16 -0
- mesa_storage/alembic/versions/fb2c3d4e5f6a_add_revision_manifest_hash.py +18 -0
- mesa_storage/alembic/versions/fc3d4e5f6a7b_add_durable_projection_operations.py +172 -0
- mesa_storage/alembic/versions/fd4e5f6a7b8c_add_embedding_provider_identity.py +20 -0
- mesa_storage/alembic/versions/fe5f6a7b8c9d_enforce_active_head_invariant.py +35 -0
- mesa_storage/alembic/versions/ff6a7b8c9d0e_freeze_revision_manifests.py +29 -0
- mesa_storage/alembic.ini +148 -0
- mesa_storage/control/__init__.py +0 -0
- mesa_storage/control/activity_repo.py +159 -0
- mesa_storage/control/approval_repo.py +141 -0
- mesa_storage/control/client_repo.py +175 -0
- mesa_storage/control/codex_profile_repo.py +105 -0
- mesa_storage/control/connection_repo.py +118 -0
- mesa_storage/control/credential_repo.py +196 -0
- mesa_storage/control/policy_repo.py +81 -0
- mesa_storage/control/settings_repo.py +56 -0
- mesa_storage/dao.py +10747 -0
- mesa_storage/embedding_identity.py +157 -0
- mesa_storage/kuzu_migration.py +450 -0
- mesa_storage/kuzu_provider.py +1768 -0
- mesa_storage/kuzu_schema_migration.py +340 -0
- mesa_storage/kuzu_setup.py +207 -0
- mesa_storage/legal_identity.py +473 -0
- mesa_storage/projection_generations.py +728 -0
- mesa_storage/rebuild_cutover.py +573 -0
- mesa_storage/rebuild_health.py +112 -0
- mesa_storage/rebuild_observability.py +92 -0
- mesa_storage/rebuild_preparation.py +533 -0
- mesa_storage/rebuild_replay.py +745 -0
- mesa_storage/recovery.py +503 -0
- mesa_storage/repositories/__init__.py +20 -0
- mesa_storage/repositories/catalog.py +230 -0
- mesa_storage/repositories/operations.py +690 -0
- mesa_storage/representation.py +99 -0
- mesa_storage/retrieval_scope.py +160 -0
- mesa_storage/schema_contract.py +453 -0
- mesa_storage/schemas.py +292 -0
- mesa_storage/sqlite_engine.py +498 -0
- mesa_storage/vector_engine.py +1697 -0
- mesa_storage/writer_lock.py +116 -0
- mesa_workers/__init__.py +6 -0
- mesa_workers/ingestion_worker.py +465 -0
- mesa_workers/projection_worker.py +322 -0
- mesa_workers/supervision.py +124 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mesa_storage.sqlite_engine import AsyncEngine
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConnectionRepository:
|
|
11
|
+
def __init__(self, sqlite_engine: AsyncEngine):
|
|
12
|
+
self._sql = sqlite_engine
|
|
13
|
+
|
|
14
|
+
async def register_connection(
|
|
15
|
+
self, connection_id: str, client_id: str, transport: str, **kwargs: Any
|
|
16
|
+
) -> None:
|
|
17
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
18
|
+
status = kwargs.get("status", "CONNECTED")
|
|
19
|
+
|
|
20
|
+
async with self._sql.transaction() as db:
|
|
21
|
+
await db.execute(
|
|
22
|
+
"""
|
|
23
|
+
INSERT INTO mcp_connections (
|
|
24
|
+
connection_id, client_id, transport, status, connected_at, last_seen_at,
|
|
25
|
+
remote_address_hash, protocol_version, client_version, user_agent, session_id, project_id
|
|
26
|
+
) VALUES (
|
|
27
|
+
:connection_id, :client_id, :transport, :status, :now, :now,
|
|
28
|
+
:remote_address_hash, :protocol_version, :client_version, :user_agent, :session_id, :project_id
|
|
29
|
+
)
|
|
30
|
+
""",
|
|
31
|
+
{
|
|
32
|
+
"connection_id": connection_id,
|
|
33
|
+
"client_id": client_id,
|
|
34
|
+
"transport": transport,
|
|
35
|
+
"status": status,
|
|
36
|
+
"now": now,
|
|
37
|
+
"remote_address_hash": kwargs.get("remote_address_hash"),
|
|
38
|
+
"protocol_version": kwargs.get("protocol_version"),
|
|
39
|
+
"client_version": kwargs.get("client_version"),
|
|
40
|
+
"user_agent": kwargs.get("user_agent"),
|
|
41
|
+
"session_id": kwargs.get("session_id"),
|
|
42
|
+
"project_id": kwargs.get("project_id"),
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
await db.commit()
|
|
46
|
+
|
|
47
|
+
async def update_connection_status(self, connection_id: str, status: str) -> None:
|
|
48
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
49
|
+
disconnected_at = (
|
|
50
|
+
now if status in ("DISCONNECTED", "REVOKED", "ERROR") else None
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async with self._sql.transaction() as db:
|
|
54
|
+
await db.execute(
|
|
55
|
+
"""
|
|
56
|
+
UPDATE mcp_connections
|
|
57
|
+
SET status = :status, last_seen_at = :now, disconnected_at = COALESCE(disconnected_at, :disconnected_at)
|
|
58
|
+
WHERE connection_id = :connection_id
|
|
59
|
+
""",
|
|
60
|
+
{
|
|
61
|
+
"connection_id": connection_id,
|
|
62
|
+
"status": status,
|
|
63
|
+
"now": now,
|
|
64
|
+
"disconnected_at": disconnected_at,
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
await db.commit()
|
|
68
|
+
|
|
69
|
+
async def get_connection(self, connection_id: str) -> dict[str, Any] | None:
|
|
70
|
+
async with self._sql.connection() as db:
|
|
71
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
72
|
+
async with db.execute(
|
|
73
|
+
"SELECT * FROM mcp_connections WHERE connection_id = :connection_id",
|
|
74
|
+
{"connection_id": connection_id},
|
|
75
|
+
) as cursor:
|
|
76
|
+
row = await cursor.fetchone()
|
|
77
|
+
return dict(row) if row else None
|
|
78
|
+
|
|
79
|
+
async def list_active_connections(
|
|
80
|
+
self, client_id: str | None = None
|
|
81
|
+
) -> list[dict[str, Any]]:
|
|
82
|
+
async with self._sql.connection() as db:
|
|
83
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
84
|
+
query = "SELECT * FROM mcp_connections WHERE status = 'CONNECTED'"
|
|
85
|
+
params: dict[str, Any] = {}
|
|
86
|
+
if client_id:
|
|
87
|
+
query += " AND client_id = :client_id"
|
|
88
|
+
params["client_id"] = client_id
|
|
89
|
+
|
|
90
|
+
async with db.execute(query, params) as cursor:
|
|
91
|
+
rows = await cursor.fetchall()
|
|
92
|
+
return [dict(row) for row in rows]
|
|
93
|
+
|
|
94
|
+
async def list_all_connections(
|
|
95
|
+
self, limit: int = 50, status: str | None = None
|
|
96
|
+
) -> list[dict[str, Any]]:
|
|
97
|
+
query = "SELECT * FROM mcp_connections WHERE 1=1"
|
|
98
|
+
params: dict[str, Any] = {}
|
|
99
|
+
if status:
|
|
100
|
+
query += " AND status = :status"
|
|
101
|
+
params["status"] = status
|
|
102
|
+
query += " ORDER BY connected_at DESC LIMIT :limit"
|
|
103
|
+
params["limit"] = min(limit, 200)
|
|
104
|
+
|
|
105
|
+
async with self._sql.connection() as db:
|
|
106
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
107
|
+
async with db.execute(query, params) as cursor:
|
|
108
|
+
rows = await cursor.fetchall()
|
|
109
|
+
return [dict(row) for row in rows]
|
|
110
|
+
|
|
111
|
+
async def count_by_status(self) -> dict[str, int]:
|
|
112
|
+
async with self._sql.connection() as db:
|
|
113
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
114
|
+
async with db.execute(
|
|
115
|
+
"SELECT status, COUNT(*) as cnt FROM mcp_connections GROUP BY status"
|
|
116
|
+
) as cursor:
|
|
117
|
+
rows = await cursor.fetchall()
|
|
118
|
+
return {row["status"]: row["cnt"] for row in rows}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Durable, binding-scoped bearer credentials for HTTP MCP clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import secrets
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from mesa_storage.sqlite_engine import AsyncEngine
|
|
12
|
+
|
|
13
|
+
DEFAULT_CREDENTIAL_TTL_SECONDS = 90 * 24 * 60 * 60
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CredentialRepository:
|
|
17
|
+
"""Issue and verify high-entropy credentials without retaining their secret."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, sqlite_engine: AsyncEngine):
|
|
20
|
+
self._sql = sqlite_engine
|
|
21
|
+
|
|
22
|
+
async def issue(
|
|
23
|
+
self,
|
|
24
|
+
client_id: str,
|
|
25
|
+
binding_id: str,
|
|
26
|
+
*,
|
|
27
|
+
token_kind: str = "codex",
|
|
28
|
+
expires_in_seconds: int = DEFAULT_CREDENTIAL_TTL_SECONDS,
|
|
29
|
+
) -> tuple[dict[str, Any], str]:
|
|
30
|
+
credential_id = f"cred_{uuid.uuid4().hex}"
|
|
31
|
+
secret = secrets.token_urlsafe(32)
|
|
32
|
+
if token_kind not in {"codex", "antigravity"}:
|
|
33
|
+
raise ValueError("unsupported credential token kind")
|
|
34
|
+
if expires_in_seconds <= 0:
|
|
35
|
+
raise ValueError("expires_in_seconds must be positive")
|
|
36
|
+
token = f"mesa_{token_kind}_{credential_id}_{secret}"
|
|
37
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
38
|
+
expires_at = (
|
|
39
|
+
datetime.now(timezone.utc) + timedelta(seconds=expires_in_seconds)
|
|
40
|
+
).isoformat()
|
|
41
|
+
record = {
|
|
42
|
+
"credential_id": credential_id,
|
|
43
|
+
"client_id": client_id,
|
|
44
|
+
"binding_id": binding_id,
|
|
45
|
+
"token_hash": _token_hash(token),
|
|
46
|
+
"token_prefix": token[:24],
|
|
47
|
+
"status": "ACTIVE",
|
|
48
|
+
"created_at": now,
|
|
49
|
+
"expires_at": expires_at,
|
|
50
|
+
}
|
|
51
|
+
async with self._sql.transaction() as db:
|
|
52
|
+
await db.execute(
|
|
53
|
+
"""
|
|
54
|
+
INSERT INTO mcp_client_credentials
|
|
55
|
+
(credential_id, client_id, binding_id, token_hash, token_prefix,
|
|
56
|
+
status, created_at, updated_at, expires_at)
|
|
57
|
+
VALUES
|
|
58
|
+
(:credential_id, :client_id, :binding_id, :token_hash, :token_prefix,
|
|
59
|
+
:status, :created_at, :created_at, :expires_at)
|
|
60
|
+
""",
|
|
61
|
+
record,
|
|
62
|
+
)
|
|
63
|
+
await db.commit()
|
|
64
|
+
return record, token
|
|
65
|
+
|
|
66
|
+
async def rotate(
|
|
67
|
+
self,
|
|
68
|
+
credential_id: str,
|
|
69
|
+
*,
|
|
70
|
+
expires_in_seconds: int = DEFAULT_CREDENTIAL_TTL_SECONDS,
|
|
71
|
+
) -> tuple[dict[str, Any], str]:
|
|
72
|
+
"""Atomically replace one active credential within its existing binding."""
|
|
73
|
+
if expires_in_seconds <= 0:
|
|
74
|
+
raise ValueError("expires_in_seconds must be positive")
|
|
75
|
+
replacement_id = f"cred_{uuid.uuid4().hex}"
|
|
76
|
+
secret = secrets.token_urlsafe(32)
|
|
77
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
78
|
+
expires_at = (
|
|
79
|
+
datetime.now(timezone.utc) + timedelta(seconds=expires_in_seconds)
|
|
80
|
+
).isoformat()
|
|
81
|
+
async with self._sql.transaction() as db:
|
|
82
|
+
async with db.execute(
|
|
83
|
+
"SELECT client_id, binding_id, token_prefix FROM mcp_client_credentials "
|
|
84
|
+
"WHERE credential_id = ? AND status = 'ACTIVE'",
|
|
85
|
+
(credential_id,),
|
|
86
|
+
) as cursor:
|
|
87
|
+
previous = await cursor.fetchone()
|
|
88
|
+
if previous is None:
|
|
89
|
+
raise ValueError("active credential not found")
|
|
90
|
+
token_kind = _token_kind_from_prefix(str(previous[2]))
|
|
91
|
+
token = f"mesa_{token_kind}_{replacement_id}_{secret}"
|
|
92
|
+
record: dict[str, Any] = {
|
|
93
|
+
"credential_id": replacement_id,
|
|
94
|
+
"client_id": str(previous[0]),
|
|
95
|
+
"binding_id": str(previous[1]),
|
|
96
|
+
"token_hash": _token_hash(token),
|
|
97
|
+
"token_prefix": token[:24],
|
|
98
|
+
"status": "ACTIVE",
|
|
99
|
+
"created_at": now,
|
|
100
|
+
"expires_at": expires_at,
|
|
101
|
+
}
|
|
102
|
+
await db.execute(
|
|
103
|
+
"INSERT INTO mcp_client_credentials "
|
|
104
|
+
"(credential_id, client_id, binding_id, token_hash, token_prefix, status, created_at, updated_at, expires_at) "
|
|
105
|
+
"VALUES (:credential_id, :client_id, :binding_id, :token_hash, :token_prefix, "
|
|
106
|
+
":status, :created_at, :created_at, :expires_at)",
|
|
107
|
+
record,
|
|
108
|
+
)
|
|
109
|
+
cursor = await db.execute(
|
|
110
|
+
"UPDATE mcp_client_credentials SET status = 'REVOKED', revoked_at = :now, updated_at = :now "
|
|
111
|
+
"WHERE credential_id = :credential_id AND status = 'ACTIVE'",
|
|
112
|
+
{"credential_id": credential_id, "now": now},
|
|
113
|
+
)
|
|
114
|
+
if cursor.rowcount != 1:
|
|
115
|
+
raise RuntimeError("credential rotation fence lost")
|
|
116
|
+
await db.commit()
|
|
117
|
+
return record, token
|
|
118
|
+
|
|
119
|
+
async def authenticate(self, token: str) -> dict[str, Any] | None:
|
|
120
|
+
digest = _token_hash(token)
|
|
121
|
+
async with self._sql.connection() as db:
|
|
122
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
123
|
+
async with db.execute(
|
|
124
|
+
"""
|
|
125
|
+
SELECT credential.*, client.enabled AS client_enabled, binding.enabled AS binding_enabled
|
|
126
|
+
FROM mcp_client_credentials AS credential
|
|
127
|
+
JOIN mcp_clients AS client ON client.client_id = credential.client_id
|
|
128
|
+
JOIN mcp_project_bindings AS binding ON binding.binding_id = credential.binding_id
|
|
129
|
+
WHERE credential.token_hash = :token_hash AND credential.status = 'ACTIVE'
|
|
130
|
+
AND (credential.expires_at IS NULL OR credential.expires_at > :now)
|
|
131
|
+
""",
|
|
132
|
+
{"token_hash": digest, "now": datetime.now(timezone.utc).isoformat()},
|
|
133
|
+
) as cursor:
|
|
134
|
+
row = await cursor.fetchone()
|
|
135
|
+
if row is None:
|
|
136
|
+
return None
|
|
137
|
+
result = dict(row)
|
|
138
|
+
if not result["client_enabled"] or not result["binding_enabled"]:
|
|
139
|
+
return None
|
|
140
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
141
|
+
async with self._sql.transaction() as db:
|
|
142
|
+
await db.execute(
|
|
143
|
+
"UPDATE mcp_client_credentials SET last_used_at = :now, updated_at = :now WHERE credential_id = :credential_id",
|
|
144
|
+
{"now": now, "credential_id": result["credential_id"]},
|
|
145
|
+
)
|
|
146
|
+
await db.commit()
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
async def revoke(self, credential_id: str) -> bool:
|
|
150
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
151
|
+
async with self._sql.transaction() as db:
|
|
152
|
+
cursor = await db.execute(
|
|
153
|
+
"""UPDATE mcp_client_credentials
|
|
154
|
+
SET status = 'REVOKED', revoked_at = :now, updated_at = :now
|
|
155
|
+
WHERE credential_id = :credential_id AND status = 'ACTIVE'""",
|
|
156
|
+
{"credential_id": credential_id, "now": now},
|
|
157
|
+
)
|
|
158
|
+
await db.commit()
|
|
159
|
+
return bool(cursor.rowcount)
|
|
160
|
+
|
|
161
|
+
async def list_for_binding(self, binding_id: str) -> list[dict[str, Any]]:
|
|
162
|
+
"""Return secret-safe credential summaries; token hashes stay private."""
|
|
163
|
+
async with self._sql.connection() as db:
|
|
164
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
165
|
+
async with db.execute(
|
|
166
|
+
"""SELECT credential_id, client_id, binding_id, token_prefix, status,
|
|
167
|
+
created_at, updated_at, last_used_at, revoked_at, expires_at
|
|
168
|
+
FROM mcp_client_credentials
|
|
169
|
+
WHERE binding_id = ? ORDER BY created_at DESC""",
|
|
170
|
+
(binding_id,),
|
|
171
|
+
) as cursor:
|
|
172
|
+
return [dict(row) for row in await cursor.fetchall()]
|
|
173
|
+
|
|
174
|
+
async def get_summary(self, credential_id: str) -> dict[str, Any] | None:
|
|
175
|
+
async with self._sql.connection() as db:
|
|
176
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
177
|
+
async with db.execute(
|
|
178
|
+
"""SELECT credential_id, client_id, binding_id, token_prefix, status,
|
|
179
|
+
created_at, updated_at, last_used_at, revoked_at, expires_at
|
|
180
|
+
FROM mcp_client_credentials WHERE credential_id = ?""",
|
|
181
|
+
(credential_id,),
|
|
182
|
+
) as cursor:
|
|
183
|
+
row = await cursor.fetchone()
|
|
184
|
+
return dict(row) if row else None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _token_hash(token: str) -> str:
|
|
188
|
+
return hashlib.sha256(token.encode("utf-8")).hexdigest()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _token_kind_from_prefix(token_prefix: str) -> str:
|
|
192
|
+
if token_prefix.startswith("mesa_antigravity_"):
|
|
193
|
+
return "antigravity"
|
|
194
|
+
if token_prefix.startswith("mesa_codex_"):
|
|
195
|
+
return "codex"
|
|
196
|
+
raise ValueError("stored credential has an unsupported token kind")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from mesa_storage.sqlite_engine import AsyncEngine
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PolicyRepository:
|
|
12
|
+
def __init__(self, sqlite_engine: AsyncEngine):
|
|
13
|
+
self._sql = sqlite_engine
|
|
14
|
+
|
|
15
|
+
async def create_rule(
|
|
16
|
+
self,
|
|
17
|
+
rule_id: str,
|
|
18
|
+
scope_type: str,
|
|
19
|
+
operation: str,
|
|
20
|
+
effect: str,
|
|
21
|
+
created_by: str,
|
|
22
|
+
scope_id: str | None = None,
|
|
23
|
+
priority: int = 100,
|
|
24
|
+
conditions: dict[str, Any] | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
27
|
+
cond_json = json.dumps(conditions or {})
|
|
28
|
+
|
|
29
|
+
async with self._sql.transaction() as db:
|
|
30
|
+
await db.execute(
|
|
31
|
+
"""
|
|
32
|
+
INSERT INTO mcp_policy_rules (
|
|
33
|
+
rule_id, scope_type, scope_id, operation, effect,
|
|
34
|
+
priority, conditions_json, created_by, created_at, updated_at
|
|
35
|
+
) VALUES (
|
|
36
|
+
:rule_id, :scope_type, :scope_id, :operation, :effect,
|
|
37
|
+
:priority, :cond_json, :created_by, :now, :now
|
|
38
|
+
)
|
|
39
|
+
""",
|
|
40
|
+
{
|
|
41
|
+
"rule_id": rule_id,
|
|
42
|
+
"scope_type": scope_type,
|
|
43
|
+
"scope_id": scope_id,
|
|
44
|
+
"operation": operation,
|
|
45
|
+
"effect": effect,
|
|
46
|
+
"priority": priority,
|
|
47
|
+
"cond_json": cond_json,
|
|
48
|
+
"created_by": created_by,
|
|
49
|
+
"now": now,
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
await db.commit()
|
|
53
|
+
|
|
54
|
+
async def get_rule(self, rule_id: str) -> dict[str, Any] | None:
|
|
55
|
+
async with self._sql.connection() as db:
|
|
56
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
57
|
+
async with db.execute(
|
|
58
|
+
"SELECT * FROM mcp_policy_rules WHERE rule_id = :rule_id",
|
|
59
|
+
{"rule_id": rule_id},
|
|
60
|
+
) as cursor:
|
|
61
|
+
row = await cursor.fetchone()
|
|
62
|
+
return dict(row) if row else None
|
|
63
|
+
|
|
64
|
+
async def list_rules(
|
|
65
|
+
self, scope_type: str | None = None, operation: str | None = None
|
|
66
|
+
) -> list[dict[str, Any]]:
|
|
67
|
+
query = "SELECT * FROM mcp_policy_rules WHERE 1=1"
|
|
68
|
+
params = {}
|
|
69
|
+
if scope_type:
|
|
70
|
+
query += " AND scope_type = :scope_type"
|
|
71
|
+
params["scope_type"] = scope_type
|
|
72
|
+
if operation:
|
|
73
|
+
query += " AND operation = :operation"
|
|
74
|
+
params["operation"] = operation
|
|
75
|
+
query += " ORDER BY priority DESC"
|
|
76
|
+
|
|
77
|
+
async with self._sql.connection() as db:
|
|
78
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
79
|
+
async with db.execute(query, params) as cursor:
|
|
80
|
+
rows = await cursor.fetchall()
|
|
81
|
+
return [dict(row) for row in rows]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from mesa_storage.sqlite_engine import AsyncEngine
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SettingsRepository:
|
|
12
|
+
def __init__(self, sqlite_engine: AsyncEngine):
|
|
13
|
+
self._sql = sqlite_engine
|
|
14
|
+
|
|
15
|
+
async def get_setting(self, setting_key: str) -> Any | None:
|
|
16
|
+
async with self._sql.connection() as db:
|
|
17
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
18
|
+
async with db.execute(
|
|
19
|
+
"SELECT setting_value_json FROM control_plane_settings WHERE setting_key = :key",
|
|
20
|
+
{"key": setting_key},
|
|
21
|
+
) as cursor:
|
|
22
|
+
row = await cursor.fetchone()
|
|
23
|
+
if not row:
|
|
24
|
+
return None
|
|
25
|
+
return json.loads(row["setting_value_json"])
|
|
26
|
+
|
|
27
|
+
async def get_all_settings(self) -> dict[str, Any]:
|
|
28
|
+
async with self._sql.connection() as db:
|
|
29
|
+
db.row_factory = __import__("aiosqlite").Row
|
|
30
|
+
async with db.execute(
|
|
31
|
+
"SELECT setting_key, setting_value_json FROM control_plane_settings"
|
|
32
|
+
) as cursor:
|
|
33
|
+
rows = await cursor.fetchall()
|
|
34
|
+
return {
|
|
35
|
+
row["setting_key"]: json.loads(row["setting_value_json"])
|
|
36
|
+
for row in rows
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async def set_setting(
|
|
40
|
+
self, setting_key: str, value: Any, updated_by: str = "system"
|
|
41
|
+
) -> None:
|
|
42
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
43
|
+
value_json = json.dumps(value)
|
|
44
|
+
async with self._sql.transaction() as db:
|
|
45
|
+
await db.execute(
|
|
46
|
+
"""
|
|
47
|
+
INSERT INTO control_plane_settings (setting_key, setting_value_json, updated_at, updated_by)
|
|
48
|
+
VALUES (:key, :val, :now, :by)
|
|
49
|
+
ON CONFLICT(setting_key) DO UPDATE SET
|
|
50
|
+
setting_value_json = excluded.setting_value_json,
|
|
51
|
+
updated_at = excluded.updated_at,
|
|
52
|
+
updated_by = excluded.updated_by
|
|
53
|
+
""",
|
|
54
|
+
{"key": setting_key, "val": value_json, "now": now, "by": updated_by},
|
|
55
|
+
)
|
|
56
|
+
await db.commit()
|