matrx-api 0.1.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.
matrx_api/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """matrx-api: the Python client of every AI Matrx server, generated from the server contract."""
2
+
3
+ from .client import (
4
+ ApiStream,
5
+ CallOptions,
6
+ JsonOrStream,
7
+ MatrxApi,
8
+ Multipart,
9
+ OperationDescriptor,
10
+ RequestInfo,
11
+ )
12
+ from .credentials import (
13
+ ApiKeyCredential,
14
+ GuestCredential,
15
+ MATRX_HEADERS,
16
+ ScopedCredential,
17
+ ServiceCredential,
18
+ UserCredential,
19
+ credential_to_headers,
20
+ )
21
+ from .deployments import CONTRACT_SHA256, DEPLOYMENTS, DOMAINS
22
+ from .errors import MatrxApiError, OrganizationContextError
23
+ from .streams import NdjsonFramer, SseFrame, SseFramer
24
+
25
+ __all__ = [
26
+ "ApiKeyCredential", "ApiStream", "CONTRACT_SHA256", "CallOptions", "DEPLOYMENTS", "DOMAINS",
27
+ "GuestCredential", "JsonOrStream", "MATRX_HEADERS", "MatrxApi", "MatrxApiError", "Multipart",
28
+ "NdjsonFramer", "OperationDescriptor", "OrganizationContextError", "RequestInfo",
29
+ "ScopedCredential", "ServiceCredential", "SseFrame", "SseFramer", "UserCredential",
30
+ "credential_to_headers",
31
+ ]
matrx_api/client.py ADDED
@@ -0,0 +1,420 @@
1
+ """``MatrxApi`` - the ONE client of every AI Matrx server (Python sister of ``createApi``).
2
+
3
+ Every generated operation hands this client a small :class:`OperationDescriptor` straight from the
4
+ server contract (method, path template, deployment, auth tier, org rule, stream kind). The client owns the rest:
5
+
6
+ - base URL per deployment (defaults from api-deployments.json, overridable per deployment);
7
+ - the injected credentials (five kinds, ``credentials.py``), re-read on every call;
8
+ - the organization binding, honouring each operation's ``x-matrx-org``: ``required`` is refused BEFORE
9
+ the wire when absent, ``optional`` sends one when bound, ``exempt`` never sends one;
10
+ - ``X-Request-Id`` (unique per call);
11
+ - reads (GET/HEAD) retry with backoff on transport failure; writes NEVER retry;
12
+ - one error model (:class:`MatrxApiError`);
13
+ - NDJSON / SSE / bytes streams as async iterators.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import json
20
+ import uuid
21
+ from collections.abc import AsyncIterator, Callable, Mapping
22
+ from dataclasses import dataclass, field
23
+ from typing import Any, Generic, Literal, TypeVar
24
+
25
+ import httpx
26
+
27
+ from .credentials import MATRX_HEADERS, CredentialsSetting, credential_to_headers, resolve_credential
28
+ from .deployments import DEPLOYMENTS
29
+ from .errors import MatrxApiError, OrganizationContextError
30
+ from .streams import NdjsonFramer, SseFramer, SseFrame
31
+
32
+ Environment = Literal["production", "localhost"]
33
+ StreamKind = Literal["none", "ndjson", "sse", "bytes", "ndjson_or_json", "sse_or_json", "bytes_or_json"]
34
+
35
+ DEFAULT_CONNECT_TIMEOUT = 15.0
36
+ E = TypeVar("E")
37
+ J = TypeVar("J")
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class OperationDescriptor:
42
+ method: str
43
+ path: str
44
+ deployment: str
45
+ auth: str
46
+ org: str
47
+ stream: str # "none" or a StreamKind
48
+ name: str # "<domain>.<op>"
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class CallOptions:
53
+ organization_id: str | None = None
54
+ headers: Mapping[str, str] | None = None
55
+ connect_timeout: float | None = None
56
+ total_timeout: float | None = None
57
+
58
+
59
+ @dataclass(frozen=True, slots=True)
60
+ class Multipart:
61
+ """A multipart/form-data body (``data`` fields + ``files`` as httpx accepts them)."""
62
+
63
+ data: Mapping[str, Any] | None = None
64
+ files: Any = None
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class RequestInfo:
69
+ operation: str
70
+ method: str
71
+ url: str
72
+ request_id: str
73
+ deployment: str
74
+
75
+
76
+ def _interpolate(template: str, params: Mapping[str, Any] | None) -> str:
77
+ import re
78
+ from urllib.parse import quote
79
+
80
+ def sub(m: "re.Match[str]") -> str:
81
+ name = m.group(1)
82
+ value = (params or {}).get(name)
83
+ if value is None or str(value) == "":
84
+ raise TypeError(f'Missing path parameter "{name}" for {template}')
85
+ return quote(str(value), safe="")
86
+
87
+ return re.sub(r"\{([^}]+)\}", sub, template)
88
+
89
+
90
+ def _query_pairs(query: Mapping[str, Any] | None) -> list[tuple[str, str]]:
91
+ out: list[tuple[str, str]] = []
92
+ for key, raw in (query or {}).items():
93
+ if raw is None:
94
+ continue
95
+ for v in raw if isinstance(raw, (list, tuple)) else [raw]:
96
+ if v is None:
97
+ continue
98
+ if isinstance(v, bool):
99
+ out.append((key, "true" if v else "false"))
100
+ elif isinstance(v, (dict, list)):
101
+ out.append((key, json.dumps(v)))
102
+ else:
103
+ out.append((key, str(v)))
104
+ return out
105
+
106
+
107
+ def _accept(stream: str) -> str:
108
+ return {
109
+ "ndjson": "application/x-ndjson",
110
+ "sse": "text/event-stream",
111
+ "ndjson_or_json": "application/x-ndjson, application/json",
112
+ "sse_or_json": "text/event-stream, application/json",
113
+ "bytes": "*/*",
114
+ "bytes_or_json": "*/*, application/json",
115
+ }.get(stream, "application/json")
116
+
117
+
118
+ class ApiStream(Generic[E]):
119
+ """An open streaming response. Iterate with ``async for``; ``aclose()`` (or the context manager) releases it."""
120
+
121
+ def __init__(self, response: httpx.Response, request_id: str, kind: str) -> None:
122
+ self.response = response
123
+ self.request_id = request_id
124
+ self._kind = kind
125
+ self._started = False
126
+
127
+ async def _iterate(self) -> AsyncIterator[Any]:
128
+ try:
129
+ if self._kind.startswith("ndjson"):
130
+ nd = NdjsonFramer()
131
+ async for chunk in self.response.aiter_bytes():
132
+ for frame in nd.push_bytes(chunk):
133
+ yield frame
134
+ for frame in nd.finish():
135
+ yield frame
136
+ elif self._kind.startswith("sse"):
137
+ sse = SseFramer()
138
+ async for chunk in self.response.aiter_bytes():
139
+ for frame in sse.push_bytes(chunk):
140
+ yield frame
141
+ for frame in sse.finish():
142
+ yield frame
143
+ else:
144
+ async for chunk in self.response.aiter_bytes():
145
+ yield chunk
146
+ except httpx.TransportError as exc:
147
+ raise MatrxApiError(status=0, path=str(self.response.url), message=f"stream interrupted: {exc}", code="network_error") from exc
148
+ finally:
149
+ await self.response.aclose()
150
+
151
+ def __aiter__(self) -> AsyncIterator[E]:
152
+ if self._started:
153
+ raise RuntimeError("an ApiStream can only be iterated once")
154
+ self._started = True
155
+ return self._iterate()
156
+
157
+ async def aclose(self) -> None:
158
+ await self.response.aclose()
159
+
160
+ async def __aenter__(self) -> "ApiStream[E]":
161
+ return self
162
+
163
+ async def __aexit__(self, *exc: object) -> None:
164
+ await self.aclose()
165
+
166
+
167
+ @dataclass(frozen=True, slots=True)
168
+ class JsonOrStream(Generic[J, E]):
169
+ """Conditional streams: ``kind`` is ``"json"`` (``data``) or ``"stream"`` (``stream``)."""
170
+
171
+ kind: Literal["json", "stream"]
172
+ data: J | None = None
173
+ stream: ApiStream[E] | None = None
174
+
175
+
176
+ class MatrxApi:
177
+ def __init__(
178
+ self,
179
+ *,
180
+ credentials: CredentialsSetting = None,
181
+ organization_id: str | None | Callable[[], str | None] = None,
182
+ environment: Environment = "production",
183
+ base_urls: Mapping[str, str] | None = None,
184
+ resolve_base_url: Callable[[str, OperationDescriptor], str | None] | None = None,
185
+ http_client: httpx.AsyncClient | None = None,
186
+ transport: httpx.AsyncBaseTransport | None = None,
187
+ connect_timeout: float = DEFAULT_CONNECT_TIMEOUT,
188
+ total_timeout: float | None = None,
189
+ retry_attempts: int = 3,
190
+ retry_backoff: float = 0.25,
191
+ request_id: Callable[[], str] | None = None,
192
+ headers: Mapping[str, str] | None = None,
193
+ on_request: Callable[[RequestInfo], None] | None = None,
194
+ sleep: Callable[[float], Any] = asyncio.sleep,
195
+ ) -> None:
196
+ self._credentials = credentials
197
+ self._organization_id = organization_id
198
+ self._environment = environment
199
+ self._base_urls = dict(base_urls or {})
200
+ self._resolve_base_url = resolve_base_url
201
+ self._owns_client = http_client is None
202
+ self._client = http_client or httpx.AsyncClient(transport=transport, follow_redirects=False)
203
+ self._connect_timeout = connect_timeout
204
+ self._total_timeout = total_timeout
205
+ self._retry_attempts = max(1, retry_attempts)
206
+ self._retry_backoff = retry_backoff
207
+ self._mint_request_id = request_id or (lambda: str(uuid.uuid4()))
208
+ self._headers = dict(headers or {})
209
+ self._on_request = on_request
210
+ self._sleep = sleep
211
+
212
+ async def aclose(self) -> None:
213
+ if self._owns_client:
214
+ await self._client.aclose()
215
+
216
+ async def __aenter__(self) -> "MatrxApi":
217
+ return self
218
+
219
+ async def __aexit__(self, *exc: object) -> None:
220
+ await self.aclose()
221
+
222
+ # -- routing -----------------------------------------------------------------------------
223
+ def base_url(self, op: OperationDescriptor) -> str:
224
+ if self._resolve_base_url:
225
+ dynamic = self._resolve_base_url(op.deployment, op)
226
+ if dynamic:
227
+ return dynamic.rstrip("/")
228
+ override = self._base_urls.get(op.deployment)
229
+ if override:
230
+ return override.rstrip("/")
231
+ dep = DEPLOYMENTS[op.deployment]
232
+ fallback = dep["base_url"].get(self._environment)
233
+ if not fallback:
234
+ server_env = dep.get("env", {}).get("server")
235
+ raise ValueError(
236
+ f'matrx_api: no base URL for deployment "{op.deployment}" in {self._environment} '
237
+ f'(operation {op.name}). Pass MatrxApi(base_urls={{"{op.deployment}": "https://..."}})'
238
+ + (f" - servers read it from {server_env}." if server_env else ".")
239
+ )
240
+ return fallback
241
+
242
+ def url(self, op: OperationDescriptor, *, path: Mapping[str, Any] | None = None) -> str:
243
+ return f"{self.base_url(op)}{_interpolate(op.path, path)}"
244
+
245
+ def _organization_for(self, op: OperationDescriptor, options: CallOptions | None) -> str | None:
246
+ if op.org == "exempt":
247
+ return None
248
+ bound = self._organization_id() if callable(self._organization_id) else self._organization_id
249
+ selected = (options.organization_id if options and options.organization_id else None) or bound
250
+ if op.org == "required":
251
+ if not isinstance(selected, str) or not selected.strip():
252
+ raise OrganizationContextError(
253
+ "organization_context_required", f"Select an organization before calling {op.name}."
254
+ )
255
+ normalized = selected.strip()
256
+ try:
257
+ uuid.UUID(normalized)
258
+ except ValueError:
259
+ raise OrganizationContextError(
260
+ "organization_context_invalid", "The selected organization ID is invalid."
261
+ ) from None
262
+ return normalized.lower()
263
+ return selected.strip() if isinstance(selected, str) and selected.strip() else None
264
+
265
+ # -- send --------------------------------------------------------------------------------
266
+ async def _build(
267
+ self,
268
+ op: OperationDescriptor,
269
+ path: Mapping[str, Any] | None,
270
+ query: Mapping[str, Any] | None,
271
+ body: Any,
272
+ options: CallOptions | None,
273
+ ) -> tuple[httpx.Request, str]:
274
+ url = self.url(op, path=path)
275
+ org = self._organization_for(op, options)
276
+ request_id = self._mint_request_id()
277
+ headers: dict[str, str] = {
278
+ **self._headers,
279
+ **(options.headers if options and options.headers else {}),
280
+ "Accept": _accept(op.stream),
281
+ }
282
+ headers[MATRX_HEADERS["request_id"]] = request_id
283
+ content: Any = None
284
+ data: Any = None
285
+ files: Any = None
286
+ json_body: Any = None
287
+ if body is not None and op.method not in ("GET", "HEAD"):
288
+ if isinstance(body, Multipart):
289
+ data, files = body.data, body.files
290
+ elif isinstance(body, (bytes, bytearray)):
291
+ content = bytes(body)
292
+ elif isinstance(body, str):
293
+ content = body.encode()
294
+ else:
295
+ json_body = body
296
+ headers.update(credential_to_headers(await resolve_credential(self._credentials)))
297
+ if org is not None:
298
+ headers[MATRX_HEADERS["organization"]] = org
299
+ if self._on_request:
300
+ self._on_request(RequestInfo(op.name, op.method, url, request_id, op.deployment))
301
+ connect = options.connect_timeout if options and options.connect_timeout is not None else self._connect_timeout
302
+ total = options.total_timeout if options and options.total_timeout is not None else self._total_timeout
303
+ # streams are long-lived: only the connect phase is bounded unless a total is requested
304
+ timeout = httpx.Timeout(total, connect=connect)
305
+ request = self._client.build_request(
306
+ op.method,
307
+ url,
308
+ params=_query_pairs(query) or None,
309
+ headers=headers,
310
+ content=content,
311
+ data=data,
312
+ files=files,
313
+ json=json_body,
314
+ timeout=timeout,
315
+ )
316
+ return request, request_id
317
+
318
+ async def _error(self, response: httpx.Response, op: OperationDescriptor, path: Mapping[str, Any] | None) -> MatrxApiError:
319
+ try:
320
+ await response.aread()
321
+ except httpx.HTTPError:
322
+ pass
323
+ try:
324
+ text = response.text
325
+ except httpx.ResponseNotRead:
326
+ text = ""
327
+ try:
328
+ detail: Any = json.loads(text) if text else None
329
+ except ValueError:
330
+ detail = text or None
331
+ await response.aclose()
332
+ return MatrxApiError(status=response.status_code, path=_interpolate(op.path, path), server_detail=detail)
333
+
334
+ async def send(
335
+ self,
336
+ op: OperationDescriptor,
337
+ *,
338
+ path: Mapping[str, Any] | None = None,
339
+ query: Mapping[str, Any] | None = None,
340
+ body: Any = None,
341
+ options: CallOptions | None = None,
342
+ stream: bool = False,
343
+ ) -> tuple[httpx.Response, str]:
344
+ """Send one operation; raises :class:`MatrxApiError` on non-2xx or transport failure."""
345
+ idempotent = op.method in ("GET", "HEAD") and not stream
346
+ attempts = self._retry_attempts if idempotent else 1
347
+ last: httpx.TransportError | None = None
348
+ for attempt in range(attempts):
349
+ request, request_id = await self._build(op, path, query, body, options)
350
+ try:
351
+ response = await self._client.send(request, stream=stream)
352
+ except httpx.TransportError as exc:
353
+ last = exc
354
+ if attempt + 1 < attempts:
355
+ await self._sleep(self._retry_backoff * (2**attempt))
356
+ continue
357
+ raise MatrxApiError(
358
+ status=0,
359
+ path=_interpolate(op.path, path),
360
+ message=f"{type(exc).__name__}: {exc}" if str(exc) else type(exc).__name__,
361
+ code="network_error",
362
+ ) from exc
363
+ if response.status_code >= 400:
364
+ raise await self._error(response, op, path)
365
+ return response, request_id
366
+ raise AssertionError(last) # pragma: no cover
367
+
368
+ async def call(
369
+ self,
370
+ op: OperationDescriptor,
371
+ *,
372
+ path: Mapping[str, Any] | None = None,
373
+ query: Mapping[str, Any] | None = None,
374
+ body: Any = None,
375
+ options: CallOptions | None = None,
376
+ ) -> Any:
377
+ """A plain JSON operation: the parsed body (None for 204/HEAD, text when not JSON)."""
378
+ response, _ = await self.send(op, path=path, query=query, body=body, options=options)
379
+ if response.status_code == 204 or op.method == "HEAD" or not response.content:
380
+ return None
381
+ if "json" in response.headers.get("content-type", ""):
382
+ return response.json()
383
+ try:
384
+ return json.loads(response.text)
385
+ except ValueError:
386
+ return response.text
387
+
388
+ async def raw(self, op: OperationDescriptor, **kwargs: Any) -> httpx.Response:
389
+ """The complete raw response (non-streamed bytes operations)."""
390
+ response, _ = await self.send(op, **kwargs)
391
+ return response
392
+
393
+ async def open_stream(
394
+ self,
395
+ op: OperationDescriptor,
396
+ *,
397
+ path: Mapping[str, Any] | None = None,
398
+ query: Mapping[str, Any] | None = None,
399
+ body: Any = None,
400
+ options: CallOptions | None = None,
401
+ ) -> ApiStream[Any]:
402
+ response, request_id = await self.send(op, path=path, query=query, body=body, options=options, stream=True)
403
+ return ApiStream(response, request_id, op.stream)
404
+
405
+ async def open_json_or_stream(
406
+ self,
407
+ op: OperationDescriptor,
408
+ *,
409
+ path: Mapping[str, Any] | None = None,
410
+ query: Mapping[str, Any] | None = None,
411
+ body: Any = None,
412
+ options: CallOptions | None = None,
413
+ ) -> JsonOrStream[Any, Any]:
414
+ response, request_id = await self.send(op, path=path, query=query, body=body, options=options, stream=True)
415
+ if "application/json" in response.headers.get("content-type", ""):
416
+ await response.aread()
417
+ data = response.json()
418
+ await response.aclose()
419
+ return JsonOrStream(kind="json", data=data)
420
+ return JsonOrStream(kind="stream", stream=ApiStream(response, request_id, op.stream))
@@ -0,0 +1,105 @@
1
+ """The credentials port: five kinds, one credential -> header mapping (mirrors @ai-matrx/auth credentials)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Awaitable, Callable
7
+ from dataclasses import dataclass
8
+ from typing import Union
9
+
10
+ MATRX_HEADERS = {
11
+ "authorization": "Authorization",
12
+ "fingerprint": "X-Fingerprint-ID",
13
+ "organization": "X-Organization-Id",
14
+ "acting_user": "X-Matrx-User-Id",
15
+ "request_id": "X-Request-Id",
16
+ }
17
+
18
+ API_KEY_PREFIX = "mx_"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class UserCredential:
23
+ """A signed-in person: ``Authorization: Bearer <Supabase JWT>``."""
24
+
25
+ access_token: str
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class GuestCredential:
30
+ """A fingerprint: ``X-Fingerprint-ID``."""
31
+
32
+ fingerprint_id: str
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class ApiKeyCredential:
37
+ """An AI Matrx API key (``mx_...``) as a Bearer token."""
38
+
39
+ api_key: str
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class ServiceCredential:
44
+ """One of our servers calling another: the server secret (+ the acting person)."""
45
+
46
+ secret: str
47
+ acting_user_id: str | None = None
48
+ secret_header: str = "Authorization"
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class ScopedCredential:
53
+ """A short-lived capability token; hand a provider so it is re-read per call."""
54
+
55
+ token: str
56
+
57
+
58
+ Credential = Union[UserCredential, GuestCredential, ApiKeyCredential, ServiceCredential, ScopedCredential]
59
+ CredentialsSetting = Union[
60
+ Credential,
61
+ None,
62
+ Callable[[], Union[Credential, None, Awaitable[Union[Credential, None]]]],
63
+ ]
64
+
65
+
66
+ def credential_to_headers(credential: Credential | None) -> dict[str, str]:
67
+ """The single place that knows which header each credential kind rides in."""
68
+ h = MATRX_HEADERS
69
+ match credential:
70
+ case None:
71
+ return {}
72
+ case UserCredential(access_token=t):
73
+ return {h["authorization"]: f"Bearer {t}"}
74
+ case GuestCredential(fingerprint_id=f):
75
+ return {h["fingerprint"]: f}
76
+ case ScopedCredential(token=t):
77
+ return {h["authorization"]: f"Bearer {t}"}
78
+ case ServiceCredential() as c:
79
+ if c.secret_header.lower() == "authorization":
80
+ out = {h["authorization"]: f"Bearer {c.secret}"}
81
+ else:
82
+ out = {c.secret_header: c.secret}
83
+ if c.acting_user_id:
84
+ out[h["acting_user"]] = c.acting_user_id
85
+ return out
86
+ case ApiKeyCredential(api_key=k):
87
+ if not k.startswith(API_KEY_PREFIX):
88
+ raise ValueError(
89
+ f'credential_to_headers: an api_key credential must start with "{API_KEY_PREFIX}" '
90
+ "(AI Matrx API keys look like mx_live_...)"
91
+ )
92
+ return {h["authorization"]: f"Bearer {k}"}
93
+ raise TypeError(f"unknown credential: {credential!r}")
94
+
95
+
96
+ async def resolve_credential(setting: CredentialsSetting) -> Credential | None:
97
+ """A fixed credential, None, or a (sync/async) provider called on every request."""
98
+ if setting is None:
99
+ return None
100
+ if callable(setting):
101
+ value = setting()
102
+ if inspect.isawaitable(value):
103
+ value = await value
104
+ return value
105
+ return setting
@@ -0,0 +1,122 @@
1
+ """GENERATED by matrx-api scripts/generate.py from aidream/api-contract - never edit by hand."""
2
+
3
+ CONTRACT_SHA256 = '7a09c07daad8cd41c7ae301d9ce90aace006a8f2d3a21dde98885807e5881da5'
4
+
5
+ # Which deployment answers (api-deployments.json): default base URLs and the env names that override them.
6
+ DEPLOYMENTS: dict[str, dict] = {
7
+ "aidream": {
8
+ "app": "aidream-main",
9
+ "base_url": {
10
+ "localhost": "http://localhost:8000",
11
+ "production": "https://server.app.matrxserver.com"
12
+ },
13
+ "env": {
14
+ "browser": "NEXT_PUBLIC_BACKEND_URL_PROD",
15
+ "sandbox_service_token": "AIDREAM_SANDBOX_SERVICE_TOKEN",
16
+ "server": "MATRX_AIDREAM_URL",
17
+ "service_token": "MATRX_AIDREAM_SERVICE_TOKEN"
18
+ }
19
+ },
20
+ "cloud-browser-streaming": {
21
+ "app": "cloud-browser-streaming",
22
+ "base_url": {},
23
+ "env": {}
24
+ },
25
+ "cloud-browser-worker": {
26
+ "app": "cloud-browser-worker",
27
+ "base_url": {},
28
+ "env": {}
29
+ },
30
+ "files": {
31
+ "app": "files-standalone",
32
+ "base_url": {
33
+ "localhost": "http://127.0.0.1:8090",
34
+ "production": "https://files.matrxserver.com"
35
+ },
36
+ "env": {
37
+ "browser": "NEXT_PUBLIC_FILES_URL",
38
+ "browser_local": "NEXT_PUBLIC_FILES_URL_LOCAL",
39
+ "server": "MATRX_FILES_URL"
40
+ }
41
+ },
42
+ "hosted-box": {
43
+ "app": "hosted-box",
44
+ "base_url": {},
45
+ "env": {}
46
+ },
47
+ "matrx-local": {
48
+ "app": "matrx-local",
49
+ "base_url": {
50
+ "localhost": "http://127.0.0.1:22140"
51
+ },
52
+ "env": {
53
+ "port_base": "MATRX_PORT_BASE"
54
+ }
55
+ },
56
+ "sandbox-orchestrator": {
57
+ "app": "sandbox-orchestrator",
58
+ "base_url": {
59
+ "production": "https://orchestrator.dev.codematrx.com"
60
+ },
61
+ "env": {
62
+ "server": "SANDBOX_ORCHESTRATOR_URL",
63
+ "service_token": "SANDBOX_ORCHESTRATOR_API_KEY"
64
+ }
65
+ },
66
+ "scraper": {
67
+ "app": "scraper-server",
68
+ "base_url": {
69
+ "localhost": "http://localhost:8001",
70
+ "production": "https://scraper.app.matrxserver.com"
71
+ },
72
+ "env": {
73
+ "browser": "NEXT_PUBLIC_SCRAPER_URL",
74
+ "browser_local": "NEXT_PUBLIC_SCRAPER_URL_LOCAL",
75
+ "server": "MATRX_SCRAPER_URL",
76
+ "service_token": "MATRX_SCRAPER_TOKEN"
77
+ }
78
+ },
79
+ "seo": {
80
+ "app": "seo-standalone",
81
+ "base_url": {
82
+ "localhost": "http://127.0.0.1:8081",
83
+ "production": "https://seo.matrxserver.com"
84
+ },
85
+ "env": {
86
+ "browser": "NEXT_PUBLIC_SEO_URL",
87
+ "browser_local": "NEXT_PUBLIC_SEO_URL_LOCAL"
88
+ }
89
+ }
90
+ }
91
+
92
+ # One generated module per x-matrx-domain (module name -> domain).
93
+ DOMAINS: dict[str, str] = {
94
+ "account": "account",
95
+ "agents": "agents",
96
+ "ai": "ai",
97
+ "apps": "apps",
98
+ "architecture": "architecture",
99
+ "audio": "audio",
100
+ "chat": "chat",
101
+ "code": "code",
102
+ "commerce": "commerce",
103
+ "communications": "communications",
104
+ "computer": "computer",
105
+ "content": "content",
106
+ "crm": "crm",
107
+ "data": "data",
108
+ "education": "education",
109
+ "files": "files",
110
+ "human_resources": "human-resources",
111
+ "integrations": "integrations",
112
+ "intelligence": "intelligence",
113
+ "knowledge": "knowledge",
114
+ "legal": "legal",
115
+ "marketing": "marketing",
116
+ "masterwork": "masterwork",
117
+ "media": "media",
118
+ "platform": "platform",
119
+ "publish": "publish",
120
+ "web": "web",
121
+ "workflows": "workflows"
122
+ }