Ruslan Magana Vsevolodovna commited on
Commit
9b060a4
·
unverified ·
2 Parent(s): 1daa4abfa0b284

Merge pull request #1 from agent-matrix/dev-v0.1.10s

Browse files
Files changed (2) hide show
  1. app/core/config.py +13 -1
  2. app/core/hub_client.py +154 -0
app/core/config.py CHANGED
@@ -1,6 +1,6 @@
1
  from __future__ import annotations
2
  import os, yaml
3
- from pydantic import BaseModel, AnyHttpUrl
4
  from typing import Optional, List
5
 
6
  class ModelCfg(BaseModel):
@@ -25,6 +25,12 @@ class RagCfg(BaseModel):
25
 
26
  class MatrixHubCfg(BaseModel):
27
  base_url: AnyHttpUrl = "https://api.matrixhub.io"
 
 
 
 
 
 
28
 
29
  class SecurityCfg(BaseModel):
30
  admin_token: Optional[str] = None
@@ -75,6 +81,12 @@ class Settings(BaseModel):
75
  if "PROVIDER_ORDER" in os.environ:
76
  settings.provider_order = [p.strip().lower() for p in os.environ["PROVIDER_ORDER"].split(",") if p.strip()]
77
 
 
 
 
 
 
 
78
  # Default to cascade
79
  if settings.chat_backend not in ("multi", "router"):
80
  settings.chat_backend = "multi"
 
1
  from __future__ import annotations
2
  import os, yaml
3
+ from pydantic import BaseModel, AnyHttpUrl, AliasChoices, Field
4
  from typing import Optional, List
5
 
6
  class ModelCfg(BaseModel):
 
25
 
26
  class MatrixHubCfg(BaseModel):
27
  base_url: AnyHttpUrl = "https://api.matrixhub.io"
28
+ # Optional token (admin endpoints are NOT expected to be used by matrix-ai)
29
+ token: Optional[str] = Field(
30
+ default=None,
31
+ validation_alias=AliasChoices("MATRIX_HUB_TOKEN", "MATRIX_TOKEN", "API_TOKEN"),
32
+ description="Bearer token if ever required (read-only usage preferred)",
33
+ )
34
 
35
  class SecurityCfg(BaseModel):
36
  admin_token: Optional[str] = None
 
81
  if "PROVIDER_ORDER" in os.environ:
82
  settings.provider_order = [p.strip().lower() for p in os.environ["PROVIDER_ORDER"].split(",") if p.strip()]
83
 
84
+ # Matrix Hub token (ecosystem-standard names)
85
+ for env_name in ("MATRIX_HUB_TOKEN", "MATRIX_TOKEN", "API_TOKEN"):
86
+ if env_name in os.environ:
87
+ settings.matrixhub.token = os.environ[env_name]
88
+ break
89
+
90
  # Default to cascade
91
  if settings.chat_backend not in ("multi", "router"):
92
  settings.chat_backend = "multi"
app/core/hub_client.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/core/hub_client.py
2
+ """
3
+ Matrix-Hub client for matrix-ai.
4
+
5
+ IMPORTANT: matrix-ai is a read-only planning/reasoning service.
6
+ It should NEVER call admin/mutation endpoints on Matrix-Hub.
7
+ This client enforces that constraint.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any, Dict, Optional
13
+
14
+ import httpx
15
+ from fastapi import HTTPException
16
+
17
+ from .config import Settings
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Endpoints that matrix-ai must NEVER call (admin/mutation operations)
22
+ FORBIDDEN_PATHS = ("/install", "/remotes", "/ingest", "/sync")
23
+
24
+
25
+ class MatrixHubClient:
26
+ """
27
+ Async HTTP client for Matrix-Hub with read-only enforcement.
28
+
29
+ - Automatically includes Bearer token if configured
30
+ - Fails fast if code attempts to call admin/mutation endpoints
31
+ - Provides operator-friendly error messages on 401/403
32
+ """
33
+
34
+ def __init__(self, settings: Optional[Settings] = None):
35
+ self._settings = settings or Settings.load()
36
+ self.base_url = str(self._settings.matrixhub.base_url).rstrip("/")
37
+ self.token = self._settings.matrixhub.token
38
+
39
+ def _assert_read_only(self, path: str) -> None:
40
+ """
41
+ Guard to prevent accidental calls to admin/mutation endpoints.
42
+ Raises RuntimeError if path contains forbidden segments.
43
+ """
44
+ if any(forbidden in path for forbidden in FORBIDDEN_PATHS):
45
+ raise RuntimeError(
46
+ f"matrix-ai must not call admin/mutation endpoints on Matrix-Hub. "
47
+ f"Attempted path: {path}"
48
+ )
49
+
50
+ def _headers(self) -> Dict[str, str]:
51
+ """Build request headers with optional Bearer token."""
52
+ headers = {"Accept": "application/json"}
53
+ if self.token:
54
+ headers["Authorization"] = f"Bearer {self.token}"
55
+ return headers
56
+
57
+ async def get(self, path: str) -> Any:
58
+ """
59
+ Perform a GET request to Matrix-Hub.
60
+
61
+ Args:
62
+ path: API path (e.g., "/catalog/agents")
63
+
64
+ Returns:
65
+ Parsed JSON response
66
+
67
+ Raises:
68
+ RuntimeError: If path is a forbidden admin endpoint
69
+ HTTPException: On HTTP errors with operator-friendly messages
70
+ """
71
+ self._assert_read_only(path)
72
+
73
+ async with httpx.AsyncClient(timeout=10) as client:
74
+ resp = await client.get(
75
+ f"{self.base_url}{path}",
76
+ headers=self._headers(),
77
+ )
78
+
79
+ if resp.status_code == 200:
80
+ return resp.json()
81
+
82
+ if resp.status_code in (401, 403):
83
+ logger.warning(
84
+ "Matrix-Hub auth error %s on %s: %s",
85
+ resp.status_code, path, resp.text
86
+ )
87
+ raise HTTPException(
88
+ status_code=resp.status_code,
89
+ detail=(
90
+ "Matrix-Hub authorization error. "
91
+ "This service should only use public/read-only endpoints. "
92
+ "If admin access is intended, set MATRIX_HUB_TOKEN."
93
+ ),
94
+ )
95
+
96
+ raise HTTPException(
97
+ status_code=resp.status_code,
98
+ detail=f"Matrix-Hub error: {resp.text}",
99
+ )
100
+
101
+ async def post(self, path: str, data: Optional[Dict[str, Any]] = None) -> Any:
102
+ """
103
+ Perform a POST request to Matrix-Hub (for read-only query endpoints only).
104
+
105
+ Args:
106
+ path: API path
107
+ data: JSON body to send
108
+
109
+ Returns:
110
+ Parsed JSON response
111
+
112
+ Raises:
113
+ RuntimeError: If path is a forbidden admin endpoint
114
+ HTTPException: On HTTP errors with operator-friendly messages
115
+ """
116
+ self._assert_read_only(path)
117
+
118
+ async with httpx.AsyncClient(timeout=10) as client:
119
+ resp = await client.post(
120
+ f"{self.base_url}{path}",
121
+ headers=self._headers(),
122
+ json=data or {},
123
+ )
124
+
125
+ if resp.status_code in (200, 201):
126
+ return resp.json()
127
+
128
+ if resp.status_code in (401, 403):
129
+ logger.warning(
130
+ "Matrix-Hub auth error %s on %s: %s",
131
+ resp.status_code, path, resp.text
132
+ )
133
+ raise HTTPException(
134
+ status_code=resp.status_code,
135
+ detail=(
136
+ "Matrix-Hub authorization error. "
137
+ "This service should only use public/read-only endpoints. "
138
+ "If admin access is intended, set MATRIX_HUB_TOKEN."
139
+ ),
140
+ )
141
+
142
+ raise HTTPException(
143
+ status_code=resp.status_code,
144
+ detail=f"Matrix-Hub error: {resp.text}",
145
+ )
146
+
147
+
148
+ # Convenience factory
149
+ def get_hub_client(settings: Optional[Settings] = None) -> MatrixHubClient:
150
+ """Create a MatrixHubClient instance."""
151
+ return MatrixHubClient(settings)
152
+
153
+
154
+ __all__ = ["MatrixHubClient", "get_hub_client", "FORBIDDEN_PATHS"]