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.
Files changed (157) hide show
  1. mesa_api/__init__.py +9 -0
  2. mesa_api/admission.py +23 -0
  3. mesa_api/identifier_validation.py +67 -0
  4. mesa_api/routers/control/router.py +276 -0
  5. mesa_api/schemas.py +16 -0
  6. mesa_api/v4_router.py +1624 -0
  7. mesa_client/__init__.py +27 -0
  8. mesa_client/client.py +1265 -0
  9. mesa_mcp/__init__.py +5 -0
  10. mesa_mcp/adapter.py +226 -0
  11. mesa_mcp/antigravity_bridge.py +466 -0
  12. mesa_mcp/antigravity_cli.py +334 -0
  13. mesa_mcp/bounded_cache.py +133 -0
  14. mesa_mcp/codex_cli.py +631 -0
  15. mesa_mcp/codex_hooks.py +161 -0
  16. mesa_mcp/configuration.py +100 -0
  17. mesa_mcp/errors.py +24 -0
  18. mesa_mcp/gateway/__init__.py +1 -0
  19. mesa_mcp/gateway/anomaly.py +30 -0
  20. mesa_mcp/gateway/app.py +270 -0
  21. mesa_mcp/gateway/approval.py +132 -0
  22. mesa_mcp/gateway/auth.py +32 -0
  23. mesa_mcp/gateway/codex_transport.py +148 -0
  24. mesa_mcp/gateway/middleware.py +182 -0
  25. mesa_mcp/gateway/operations.py +955 -0
  26. mesa_mcp/gateway/policy/__init__.py +1 -0
  27. mesa_mcp/gateway/policy/engine.py +80 -0
  28. mesa_mcp/gateway/policy/simulator.py +45 -0
  29. mesa_mcp/http_service.py +63 -0
  30. mesa_mcp/local_cli.py +374 -0
  31. mesa_mcp/security.py +68 -0
  32. mesa_mcp/server.py +317 -0
  33. mesa_mcp/service.py +25 -0
  34. mesa_mcp/v4_service.py +582 -0
  35. mesa_mcp/workspace.py +23 -0
  36. mesa_memory/adapter/base.py +49 -0
  37. mesa_memory/adapter/claude.py +220 -0
  38. mesa_memory/adapter/data/cl100k_base.tiktoken +100256 -0
  39. mesa_memory/adapter/endpoints.py +14 -0
  40. mesa_memory/adapter/factory.py +288 -0
  41. mesa_memory/adapter/live.py +274 -0
  42. mesa_memory/adapter/ollama.py +113 -0
  43. mesa_memory/adapter/tokenizer.py +98 -0
  44. mesa_memory/api/middleware.py +91 -0
  45. mesa_memory/api/server.py +926 -0
  46. mesa_memory/config.py +993 -0
  47. mesa_memory/consolidation/loop.py +1179 -0
  48. mesa_memory/consolidation/parser.py +271 -0
  49. mesa_memory/consolidation/policy.py +173 -0
  50. mesa_memory/consolidation/router.py +392 -0
  51. mesa_memory/consolidation/schemas.py +255 -0
  52. mesa_memory/consolidation/validator.py +286 -0
  53. mesa_memory/container_health.py +25 -0
  54. mesa_memory/context_builder.py +693 -0
  55. mesa_memory/document_chunking.py +99 -0
  56. mesa_memory/embedding/__init__.py +21 -0
  57. mesa_memory/embedding/service.py +752 -0
  58. mesa_memory/extraction/service.py +614 -0
  59. mesa_memory/graph/__init__.py +5 -0
  60. mesa_memory/graph/projector.py +43 -0
  61. mesa_memory/observability/http.py +122 -0
  62. mesa_memory/observability/logger.py +259 -0
  63. mesa_memory/observability/metrics.py +252 -0
  64. mesa_memory/rebuild_runner.py +505 -0
  65. mesa_memory/retrieval/adaptive.py +311 -0
  66. mesa_memory/retrieval/core.py +2 -0
  67. mesa_memory/retrieval/legal_resolver.py +15 -0
  68. mesa_memory/runtime_entrypoint.py +49 -0
  69. mesa_memory/security/admin_cli.py +204 -0
  70. mesa_memory/security/api_keys.py +267 -0
  71. mesa_memory/security/input_validation.py +50 -0
  72. mesa_memory/security/rbac.py +787 -0
  73. mesa_memory/security/rbac_constants.py +25 -0
  74. mesa_memory/security/untrusted_memory.py +45 -0
  75. mesa_memory/utils.py +36 -0
  76. mesa_memory-0.9.0.dist-info/METADATA +213 -0
  77. mesa_memory-0.9.0.dist-info/RECORD +157 -0
  78. mesa_memory-0.9.0.dist-info/WHEEL +5 -0
  79. mesa_memory-0.9.0.dist-info/entry_points.txt +8 -0
  80. mesa_memory-0.9.0.dist-info/licenses/LICENSE +21 -0
  81. mesa_memory-0.9.0.dist-info/top_level.txt +6 -0
  82. mesa_storage/__init__.py +42 -0
  83. mesa_storage/alembic/README +1 -0
  84. mesa_storage/alembic/env.py +133 -0
  85. mesa_storage/alembic/script.py.mako +28 -0
  86. mesa_storage/alembic/versions/076eef5d1b6c_add_daily_limits.py +34 -0
  87. mesa_storage/alembic/versions/087de6628c51_add_mcp_activity_approval.py +81 -0
  88. mesa_storage/alembic/versions/0a7b8c9d0e1f_scope_catalog_physical_ids.py +42 -0
  89. mesa_storage/alembic/versions/1e7a061f7f9e_add_mcp_clients_and_bindings.py +61 -0
  90. mesa_storage/alembic/versions/41402f316580_add_temporal_validity_to_nodes.py +30 -0
  91. mesa_storage/alembic/versions/4933fb5fd0ea_initial_schema.py +143 -0
  92. mesa_storage/alembic/versions/9a1b2c3d4e5f_add_v4_catalog_provenance.py +367 -0
  93. mesa_storage/alembic/versions/9c6c7ae69ed7_add_mcp_policy_rules.py +41 -0
  94. mesa_storage/alembic/versions/a1d2e3f4b5c6_add_wal_projection_states.py +53 -0
  95. mesa_storage/alembic/versions/a2b3c4d5e6f7_add_declared_content_hash.py +24 -0
  96. mesa_storage/alembic/versions/a94df5d14fce_add_connections_and_settings.py +55 -0
  97. mesa_storage/alembic/versions/b2e3f4a5c6d7_add_session_finalization_journal.py +32 -0
  98. mesa_storage/alembic/versions/b3c4d5e6f7a8_allow_unknown_revision_content_hash.py +29 -0
  99. mesa_storage/alembic/versions/bb2355d0cdd4_add_epistemic_columns.py +38 -0
  100. mesa_storage/alembic/versions/c3d4e5f6a7b8_replace_daily_limit_credential_subject.py +45 -0
  101. mesa_storage/alembic/versions/c4d5e6f7a8b9_add_assertion_fts_and_representation.py +119 -0
  102. mesa_storage/alembic/versions/c4f1a8e2d9b0_add_purge_journal.py +62 -0
  103. mesa_storage/alembic/versions/c5d6e7f8a9b0_add_mcp_operation_ledger.py +58 -0
  104. mesa_storage/alembic/versions/d4e5f6a7b8c9_add_v4_mutation_ledger.py +96 -0
  105. mesa_storage/alembic/versions/d5e6f7a8b9c0_add_typed_legal_identity.py +65 -0
  106. mesa_storage/alembic/versions/d6e7f8a9b0c1_add_mcp_client_credentials.py +35 -0
  107. mesa_storage/alembic/versions/e7f8a9b0c1d2_add_mcp_codex_profiles.py +33 -0
  108. mesa_storage/alembic/versions/e9b7c3a1d4f2_add_claim_leases.py +61 -0
  109. mesa_storage/alembic/versions/f6d4a7b8c9e0_add_dispatch_journal.py +48 -0
  110. mesa_storage/alembic/versions/f7e5b9c0d1a2_add_dispatch_queue_payload_bytes.py +33 -0
  111. mesa_storage/alembic/versions/f8a6c0d1e2b3_add_dispatch_completion_receipts.py +44 -0
  112. mesa_storage/alembic/versions/f8a9b0c1d2e3_generalize_mcp_context_profiles.py +41 -0
  113. mesa_storage/alembic/versions/f9a0b1c2d3e4_enforce_catalog_scope_integrity.py +44 -0
  114. mesa_storage/alembic/versions/fa1b2c3d4e5f_add_mcp_credential_expiry.py +16 -0
  115. mesa_storage/alembic/versions/fb2c3d4e5f6a_add_revision_manifest_hash.py +18 -0
  116. mesa_storage/alembic/versions/fc3d4e5f6a7b_add_durable_projection_operations.py +172 -0
  117. mesa_storage/alembic/versions/fd4e5f6a7b8c_add_embedding_provider_identity.py +20 -0
  118. mesa_storage/alembic/versions/fe5f6a7b8c9d_enforce_active_head_invariant.py +35 -0
  119. mesa_storage/alembic/versions/ff6a7b8c9d0e_freeze_revision_manifests.py +29 -0
  120. mesa_storage/alembic.ini +148 -0
  121. mesa_storage/control/__init__.py +0 -0
  122. mesa_storage/control/activity_repo.py +159 -0
  123. mesa_storage/control/approval_repo.py +141 -0
  124. mesa_storage/control/client_repo.py +175 -0
  125. mesa_storage/control/codex_profile_repo.py +105 -0
  126. mesa_storage/control/connection_repo.py +118 -0
  127. mesa_storage/control/credential_repo.py +196 -0
  128. mesa_storage/control/policy_repo.py +81 -0
  129. mesa_storage/control/settings_repo.py +56 -0
  130. mesa_storage/dao.py +10747 -0
  131. mesa_storage/embedding_identity.py +157 -0
  132. mesa_storage/kuzu_migration.py +450 -0
  133. mesa_storage/kuzu_provider.py +1768 -0
  134. mesa_storage/kuzu_schema_migration.py +340 -0
  135. mesa_storage/kuzu_setup.py +207 -0
  136. mesa_storage/legal_identity.py +473 -0
  137. mesa_storage/projection_generations.py +728 -0
  138. mesa_storage/rebuild_cutover.py +573 -0
  139. mesa_storage/rebuild_health.py +112 -0
  140. mesa_storage/rebuild_observability.py +92 -0
  141. mesa_storage/rebuild_preparation.py +533 -0
  142. mesa_storage/rebuild_replay.py +745 -0
  143. mesa_storage/recovery.py +503 -0
  144. mesa_storage/repositories/__init__.py +20 -0
  145. mesa_storage/repositories/catalog.py +230 -0
  146. mesa_storage/repositories/operations.py +690 -0
  147. mesa_storage/representation.py +99 -0
  148. mesa_storage/retrieval_scope.py +160 -0
  149. mesa_storage/schema_contract.py +453 -0
  150. mesa_storage/schemas.py +292 -0
  151. mesa_storage/sqlite_engine.py +498 -0
  152. mesa_storage/vector_engine.py +1697 -0
  153. mesa_storage/writer_lock.py +116 -0
  154. mesa_workers/__init__.py +6 -0
  155. mesa_workers/ingestion_worker.py +465 -0
  156. mesa_workers/projection_worker.py +322 -0
  157. mesa_workers/supervision.py +124 -0
mesa_api/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Versioned MESA HTTP contracts.
2
+
3
+ ``mesa_api.v4_router`` exposes the breaking full-cognitive catalog, immutable
4
+ session, mutation, provenance, search, replay and rollback contract.
5
+ """
6
+
7
+ from .v4_router import create_v4_router
8
+
9
+ __all__ = ["create_v4_router"]
mesa_api/admission.py ADDED
@@ -0,0 +1,23 @@
1
+ """Shared API admission policy for storage-root mutations."""
2
+
3
+ from fastapi import HTTPException
4
+
5
+ from mesa_storage.dao import MemoryDAO
6
+
7
+
8
+ async def require_mutation_admission(
9
+ dao: MemoryDAO, *, require_projection_consumer: bool = False
10
+ ) -> None:
11
+ """Fail closed while a durable projection rebuild owns the storage root."""
12
+ if require_projection_consumer and dao.canonical_v4_writes_enabled is False:
13
+ raise HTTPException(
14
+ status_code=503,
15
+ detail="canonical_processing_unavailable",
16
+ headers={"Retry-After": "5"},
17
+ )
18
+ if await dao.rebuild_admission.is_pending():
19
+ raise HTTPException(
20
+ status_code=503,
21
+ detail="maintenance_pending",
22
+ headers={"Retry-After": "5"},
23
+ )
@@ -0,0 +1,67 @@
1
+ """Shared public identifier constraints for versioned API boundaries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Annotated
7
+
8
+ from pydantic import AfterValidator, Field
9
+
10
+ _RESERVED_SENTINELS = frozenset({"__unset__", "__system__", ""})
11
+ _ALLOWED_IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z0-9._-]+$")
12
+ _CONTROL_CHAR_PATTERN = re.compile(r"[\x00-\x1f\x7f]")
13
+
14
+
15
+ def validate_public_identifier(
16
+ value: str,
17
+ field_name: str = "identifier",
18
+ *,
19
+ max_length: int = 128,
20
+ restricted_charset: bool = True,
21
+ ) -> str:
22
+ """Validate one caller-controlled public identifier.
23
+
24
+ The 128-character form covers tenant, workspace, dataset, agent, session,
25
+ mutation, and operation identifiers. Source-owned document, revision, and
26
+ chunk identifiers retain their established 256-character boundary.
27
+ """
28
+ if _CONTROL_CHAR_PATTERN.search(value):
29
+ raise ValueError(
30
+ f"{field_name} contains illegal control characters. "
31
+ "Only printable ASCII characters are allowed."
32
+ )
33
+
34
+ stripped = value.strip()
35
+ if stripped in _RESERVED_SENTINELS:
36
+ raise ValueError(f"{field_name} cannot be empty or a reserved value")
37
+ if len(stripped) > max_length:
38
+ raise ValueError(f"{field_name} must be between 1 and {max_length} characters")
39
+ if restricted_charset and not _ALLOWED_IDENTIFIER_PATTERN.fullmatch(stripped):
40
+ raise ValueError(
41
+ f"{field_name} may contain only alphanumeric characters, hyphens, "
42
+ "underscores, or dots"
43
+ )
44
+ return stripped
45
+
46
+
47
+ def _validate_standard_identifier(value: str) -> str:
48
+ return validate_public_identifier(value)
49
+
50
+
51
+ def _validate_source_identifier(value: str) -> str:
52
+ # Document/revision/chunk aliases historically permit external naming
53
+ # punctuation. Preserve that compatibility while enforcing the shared
54
+ # control-character, sentinel, and size boundaries.
55
+ return validate_public_identifier(value, max_length=256, restricted_charset=False)
56
+
57
+
58
+ PublicIdentifier = Annotated[
59
+ str,
60
+ Field(min_length=1, max_length=128),
61
+ AfterValidator(_validate_standard_identifier),
62
+ ]
63
+ SourceIdentifier = Annotated[
64
+ str,
65
+ Field(min_length=1, max_length=256),
66
+ AfterValidator(_validate_source_identifier),
67
+ ]
@@ -0,0 +1,276 @@
1
+ # mypy: disable-error-code="no-untyped-def,untyped-decorator"
2
+ from typing import Any, Callable
3
+
4
+ from fastapi import APIRouter, Depends, HTTPException, Request
5
+ from pydantic import BaseModel
6
+
7
+
8
+ def create_control_router(
9
+ get_client_repo: Callable,
10
+ get_conn_repo: Callable,
11
+ get_settings_repo: Callable,
12
+ get_policy_repo: Callable,
13
+ get_activity_repo: Callable,
14
+ get_approval_repo: Callable,
15
+ get_credential_repo: Callable | None = None,
16
+ get_codex_profile_repo: Callable | None = None,
17
+ get_access_control: Callable | None = None,
18
+ prefix: str = "/control/mcp",
19
+ ) -> APIRouter:
20
+ async def require_control_admin(request: Request) -> None:
21
+ principal = getattr(request.state, "principal", None)
22
+ if principal is None or getattr(principal, "status", None) != "active":
23
+ raise HTTPException(
24
+ status_code=401, detail="Active authenticated principal required"
25
+ )
26
+ if get_access_control is None:
27
+ raise HTTPException(status_code=503, detail="Control authorization unavailable")
28
+ if not await get_access_control().check_control_role(
29
+ str(principal.principal_id), "ADMIN"
30
+ ):
31
+ raise HTTPException(status_code=403, detail="Control administrator role required")
32
+
33
+ router = APIRouter(
34
+ prefix=prefix,
35
+ tags=["mcp-control"],
36
+ dependencies=[Depends(require_control_admin)],
37
+ )
38
+
39
+ class ClientCreateReq(BaseModel):
40
+ client_id: str
41
+ display_name: str
42
+ client_type: str
43
+ principal_id: str
44
+ metadata: dict[str, Any] = {}
45
+
46
+ @router.post("/clients", status_code=201)
47
+ async def create_client(req: ClientCreateReq, repo=Depends(get_client_repo)):
48
+ await repo.create_client(
49
+ client_id=req.client_id,
50
+ display_name=req.display_name,
51
+ client_type=req.client_type,
52
+ principal_id=req.principal_id,
53
+ metadata=req.metadata,
54
+ )
55
+ return {"status": "created", "client_id": req.client_id}
56
+
57
+ @router.get("/clients")
58
+ async def list_clients(repo=Depends(get_client_repo)):
59
+ clients = await repo.list_clients()
60
+ return {"clients": clients}
61
+
62
+ @router.get("/clients/{client_id}")
63
+ async def get_client(client_id: str, repo=Depends(get_client_repo)):
64
+ client = await repo.get_client(client_id)
65
+ if not client:
66
+ raise HTTPException(status_code=404, detail="Client not found")
67
+ return client
68
+
69
+ @router.get("/connections")
70
+ async def list_connections(
71
+ client_id: str | None = None, repo=Depends(get_conn_repo)
72
+ ):
73
+ conns = await repo.list_active_connections(client_id=client_id)
74
+ return {"connections": conns}
75
+
76
+ @router.get("/settings")
77
+ async def get_settings(repo=Depends(get_settings_repo)):
78
+ settings = await repo.get_all_settings()
79
+ return {"settings": settings}
80
+
81
+ @router.post("/settings")
82
+ async def update_setting(key: str, value: Any, repo=Depends(get_settings_repo)):
83
+ await repo.set_setting(key, value)
84
+ return {"status": "updated", "key": key}
85
+
86
+ class PolicyCreateReq(BaseModel):
87
+ rule_id: str
88
+ scope_type: str
89
+ operation: str
90
+ effect: str
91
+ created_by: str
92
+ scope_id: str | None = None
93
+ priority: int = 100
94
+ conditions: dict[str, Any] = {}
95
+
96
+ @router.post("/policies", status_code=201)
97
+ async def create_policy(req: PolicyCreateReq, repo=Depends(get_policy_repo)):
98
+ await repo.create_rule(
99
+ rule_id=req.rule_id,
100
+ scope_type=req.scope_type,
101
+ operation=req.operation,
102
+ effect=req.effect,
103
+ created_by=req.created_by,
104
+ scope_id=req.scope_id,
105
+ priority=req.priority,
106
+ conditions=req.conditions,
107
+ )
108
+ return {"status": "created", "rule_id": req.rule_id}
109
+
110
+ @router.get("/policies")
111
+ async def list_policies(
112
+ operation: str | None = None, repo=Depends(get_policy_repo)
113
+ ):
114
+ rules = await repo.list_rules(operation=operation)
115
+ return {"rules": rules}
116
+
117
+ @router.put("/clients/{client_id}/enabled")
118
+ async def toggle_client_enabled(
119
+ client_id: str, enabled: bool, repo=Depends(get_client_repo)
120
+ ):
121
+ await repo.toggle_client_enabled(client_id, enabled)
122
+ return {"status": "updated", "client_id": client_id, "enabled": enabled}
123
+
124
+ @router.get("/clients/{client_id}/bindings")
125
+ async def list_bindings(client_id: str, repo=Depends(get_client_repo)):
126
+ bindings = await repo.list_bindings(client_id)
127
+ return {"bindings": bindings}
128
+
129
+ @router.get("/managed-clients")
130
+ @router.get("/codex", include_in_schema=False)
131
+ async def list_managed_clients(
132
+ client_repo=Depends(get_client_repo),
133
+ conn_repo=Depends(get_conn_repo),
134
+ approval_repo=Depends(get_approval_repo),
135
+ ):
136
+ """Secret-safe Codex state; credentials are summaries only."""
137
+ if get_credential_repo is None or get_codex_profile_repo is None:
138
+ raise HTTPException(
139
+ status_code=503, detail="Codex control plane unavailable"
140
+ )
141
+ credential_repo = get_credential_repo()
142
+ profile_repo = get_codex_profile_repo()
143
+ active = await conn_repo.list_active_connections()
144
+ pending = await approval_repo.list_pending_approvals()
145
+ result = []
146
+ for client in await client_repo.list_clients():
147
+ if client.get("client_type") not in {"codex", "antigravity"}:
148
+ continue
149
+ bindings = await client_repo.list_bindings(client["client_id"])
150
+ entries = []
151
+ for binding in bindings:
152
+ entries.append(
153
+ {
154
+ "binding": binding,
155
+ "profile": await profile_repo.get(binding["binding_id"]),
156
+ "credentials": await credential_repo.list_for_binding(
157
+ binding["binding_id"]
158
+ ),
159
+ "active_connections": sum(
160
+ 1
161
+ for connection in active
162
+ if connection["client_id"] == client["client_id"]
163
+ and connection.get("project_id")
164
+ == binding["external_project_id"]
165
+ ),
166
+ "pending_approvals": sum(
167
+ 1
168
+ for approval in pending
169
+ if approval["client_id"] == client["client_id"]
170
+ ),
171
+ }
172
+ )
173
+ result.append({"client": client, "bindings": entries})
174
+ return {"clients": result}
175
+
176
+ @router.post("/credentials/{credential_id}/revoke")
177
+ @router.post("/codex/credentials/{credential_id}/revoke", include_in_schema=False)
178
+ async def revoke_managed_credential(credential_id: str):
179
+ if get_credential_repo is None:
180
+ raise HTTPException(
181
+ status_code=503, detail="Codex control plane unavailable"
182
+ )
183
+ credential = await get_credential_repo().get_summary(credential_id)
184
+ if credential is None:
185
+ raise HTTPException(status_code=404, detail="Credential not found")
186
+ if not await get_credential_repo().revoke(credential_id):
187
+ raise HTTPException(status_code=409, detail="Credential is not active")
188
+ return {"status": "revoked", "credential_id": credential_id}
189
+
190
+ @router.get("/activity")
191
+ async def list_activity(
192
+ limit: int = 50,
193
+ offset: int = 0,
194
+ client_id: str | None = None,
195
+ status: str | None = None,
196
+ repo=Depends(get_activity_repo),
197
+ ):
198
+ calls = await repo.list_recent_calls(
199
+ limit=limit, offset=offset, client_id=client_id, status=status
200
+ )
201
+ return {"activity": calls}
202
+
203
+ @router.get("/activity/{call_id}")
204
+ async def get_activity_call(call_id: str, repo=Depends(get_activity_repo)):
205
+ call = await repo.get_call(call_id)
206
+ if not call:
207
+ raise HTTPException(status_code=404, detail="Call not found")
208
+ return call
209
+
210
+ @router.get("/approvals")
211
+ async def list_approvals(
212
+ status: str | None = None,
213
+ limit: int = 50,
214
+ offset: int = 0,
215
+ repo=Depends(get_approval_repo),
216
+ ):
217
+ approvals = await repo.list_approvals(status=status, limit=limit, offset=offset)
218
+ return {"approvals": approvals}
219
+
220
+ @router.get("/approvals/pending")
221
+ async def list_pending_approvals_endpoint(
222
+ client_id: str | None = None, repo=Depends(get_approval_repo)
223
+ ):
224
+ approvals = await repo.list_pending_approvals(client_id=client_id)
225
+ return {"approvals": approvals}
226
+
227
+ class DecideApprovalReq(BaseModel):
228
+ status: str
229
+ reason: str | None = None
230
+
231
+ @router.post("/approvals/{approval_id}/decide")
232
+ async def decide_approval_endpoint(
233
+ approval_id: str,
234
+ req: DecideApprovalReq,
235
+ request: Request,
236
+ repo=Depends(get_approval_repo),
237
+ ):
238
+ if req.status not in ("APPROVED", "REJECTED"):
239
+ raise HTTPException(
240
+ status_code=400, detail="Status must be APPROVED or REJECTED"
241
+ )
242
+ principal = getattr(request.state, "principal", None)
243
+ if principal is None or getattr(principal, "status", None) != "active":
244
+ raise HTTPException(
245
+ status_code=401, detail="Active authenticated principal required"
246
+ )
247
+ decided = await repo.decide_approval(
248
+ approval_id, req.status, str(principal.principal_id), req.reason
249
+ )
250
+ if not decided:
251
+ if await repo.get_approval_request(approval_id) is None:
252
+ raise HTTPException(status_code=404, detail="Approval not found")
253
+ raise HTTPException(status_code=409, detail="Approval is no longer pending")
254
+ return {"status": "decided", "approval_id": approval_id, "decision": req.status}
255
+
256
+ @router.get("/overview")
257
+ async def get_overview(
258
+ client_repo=Depends(get_client_repo),
259
+ conn_repo=Depends(get_conn_repo),
260
+ activity_repo=Depends(get_activity_repo),
261
+ approval_repo=Depends(get_approval_repo),
262
+ ):
263
+ clients = await client_repo.list_clients()
264
+ conns = await conn_repo.count_by_status()
265
+ pending = await approval_repo.count_pending()
266
+ calls = await activity_repo.count_calls_by_status()
267
+
268
+ return {
269
+ "total_clients": len(clients),
270
+ "active_clients": len([c for c in clients if c.get("enabled")]),
271
+ "connections_by_status": conns,
272
+ "pending_approvals": pending,
273
+ "calls_by_status": calls,
274
+ }
275
+
276
+ return router
mesa_api/schemas.py ADDED
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class ErrorResponse(BaseModel):
7
+ """Standardised error response for all API error paths."""
8
+
9
+ model_config = ConfigDict(frozen=True)
10
+
11
+ error: str = Field(..., description="Machine-readable error code")
12
+ detail: str = Field(..., description="Human-readable error description")
13
+ status_code: int = Field(..., ge=400, le=599)
14
+ retryable: bool = Field(
15
+ default=False, description="Whether retrying unchanged may succeed later"
16
+ )