-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathschema.py
More file actions
299 lines (225 loc) · 10.1 KB
/
Copy pathschema.py
File metadata and controls
299 lines (225 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
"""Schema definitions for the modulex-integrations package.
Every integration declares its metadata by constructing an
``IntegrationManifest`` instance in its own ``manifest.py``. The modulex
runtime imports these manifests via the ``modulex.tools`` entry-point
group and uses them to drive credential UI, validation, and tool
discovery.
Design choices worth knowing:
- All models use ``extra="forbid"`` so a typo in a contributor's
manifest fails at import time, not at runtime.
- ``auth_schemas`` is a discriminated union keyed on ``auth_type``;
the same integration can expose multiple credential methods (GitHub
supports both OAuth2 and a personal access token).
- Action ``output_schema`` is intentionally absent. The shape of every
action's return value is derived from the LangChain ``@tool``
function's return-type annotation (defined in each integration's
``outputs.py``); the runtime obtains it via
``Model.model_json_schema()`` at startup.
"""
from __future__ import annotations
from typing import Annotated, Any, Literal
from pydantic import BaseModel, ConfigDict, Field
__all__ = [
"ActionDefinition",
"ApiKeyAuthSchema",
"AuthSchema",
"BasicAuthSpec",
"BearerTokenAuthSchema",
"CustomAuthSchema",
"EnvVar",
"IntegrationManifest",
"InternalAuthSchema",
"ModulexKeyAuthSchema",
"OAuth2AuthSchema",
"OAuthConfig",
"ParameterDef",
"SuccessIndicators",
"TestEndpoint",
]
# --- Parameters --------------------------------------------------------
ParameterType = Literal[
"string",
"integer",
"number",
"boolean",
"array",
"object",
]
class ParameterDef(BaseModel):
"""Definition of a single action parameter."""
model_config = ConfigDict(extra="forbid")
type: ParameterType
description: str
default: Any = None
required: bool = False
# --- Actions -----------------------------------------------------------
class ActionDefinition(BaseModel):
"""One callable action exposed by the integration.
The return shape is derived from the matching ``@tool`` function's
return-type annotation in ``tools.py`` — do not duplicate it here.
"""
model_config = ConfigDict(extra="forbid")
name: str = Field(pattern=r"^[a-z][a-z0-9_]*$")
description: str
parameters: dict[str, ParameterDef] = Field(default_factory=dict)
# --- Credentials -------------------------------------------------------
class EnvVar(BaseModel):
"""A configurable secret/setting the operator must provide."""
model_config = ConfigDict(extra="forbid")
name: str
display_name: str
description: str
required: bool = True
sensitive: bool = False
only_for_custom: bool = False
# When True, the runtime must guarantee this value is present in the
# credential's ``auth_data`` at action-execution time, so a ``tools.py``
# function can read it. The source is *derived*, not declared:
# - ``only_for_custom=False`` -> per-credential user input; the runtime
# persists the user-entered value into ``auth_data`` at credential
# creation (e.g. Google Merchant Center ``merchant_id``).
# - ``only_for_custom=True`` -> server-level secret; for the managed
# app the runtime resolves it from the server environment and injects
# it at credential-resolution time, while a bring-your-own-app user
# supplies their own (e.g. Google Ads ``developer_token``).
# Tools read the value via the normalized (prefix-stripped, lowercased)
# key. Default False preserves today's behavior: the EnvVar is used only
# for OAuth provider config and the credential test endpoint, never for
# action calls.
inject_into_auth_data: bool = False
sample_format: str | None = None
about_url: str | None = None
class SuccessIndicators(BaseModel):
"""How to tell a credential test endpoint succeeded."""
model_config = ConfigDict(extra="forbid")
status_codes: list[int]
response_fields: list[str] | None = None
class BasicAuthSpec(BaseModel):
"""Declarative HTTP Basic Auth synthesis for ``TestEndpoint``.
Use this when the credential test endpoint requires
``Authorization: Basic <base64(username:password)>`` and the
Base64 part can't be computed at manifest authoring time (because
one or both halves are user-supplied secrets).
The modulex runtime resolves each placeholder against ``auth_data``
and constructs the ``Authorization`` header at test time. If the
placeholder name is NOT a key in ``auth_data``, it is treated as
a literal string (Mailgun: ``username_placeholder="api"``).
Examples:
* Mailgun: ``BasicAuthSpec(username_placeholder="api",
password_placeholder="MAILGUN_API_KEY")``
* Twilio: ``BasicAuthSpec(username_placeholder="TWILIO_ACCOUNT_SID",
password_placeholder="TWILIO_AUTH_TOKEN")``
"""
model_config = ConfigDict(extra="forbid")
type: Literal["basic"] = "basic"
username_placeholder: str
password_placeholder: str
class TestEndpoint(BaseModel):
"""Endpoint hit to validate a configured credential.
URL, header, body, and query-parameter values may contain
placeholders such as ``{access_token}``, ``{token}``, or
``{api_key}`` which the runtime substitutes with the resolved
credential value. ``body`` is sent as a JSON payload on non-GET
methods (e.g. POST-based credential checks such as Exa's
``POST /search``). ``params`` is the URL query string used by
integrations that pass their credential as a query parameter
(e.g. ConvertAPI's ``?Secret={api_key}``, Nasdaq's
``?api_key={api_key}``).
``auth`` is an optional declarative directive for Basic Auth
synthesis — use it for endpoints that require
``Authorization: Basic <base64(u:p)>`` (Mailgun, Twilio,
Customer.io tracking API, etc.). When set, the modulex runtime
builds the ``Authorization`` header itself and ignores any
pre-existing ``Authorization`` entry in ``headers``.
"""
__test__ = False # pydantic model, not a pytest test class
model_config = ConfigDict(extra="forbid")
url: str
method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"] = "GET"
headers: dict[str, str] = Field(default_factory=dict)
params: dict[str, str] = Field(default_factory=dict)
body: dict[str, Any] | None = None
auth: BasicAuthSpec | None = None
success_indicators: SuccessIndicators
cost_level: str = "free"
description: str | None = None
# --- Auth schemas (discriminated union) --------------------------------
class _AuthSchemaBase(BaseModel):
"""Fields shared by every auth_schema variant.
``test_endpoint`` is optional: some legacy integrations (e.g.
instacart, hackernews) use ``modulex_key`` for a "public API"
that has no credential to validate, so they ship no
test_endpoint at all. The modulex runtime skips credential
testing in that case.
"""
model_config = ConfigDict(extra="forbid")
display_name: str
description: str
setup_instructions: list[str] | None = None
setup_environment_variables: list[EnvVar] = Field(default_factory=list)
test_endpoint: TestEndpoint | None = None
class OAuthConfig(BaseModel):
"""OAuth 2.0 authorize/token URL bundle."""
model_config = ConfigDict(extra="forbid")
auth_url: str
token_url: str
scopes: list[str] = Field(default_factory=list)
token_auth_method: Literal["body", "basic"] = "body"
# Extra OAuth *authorize*-URL params for providers that require an
# explicit opt-in to refresh tokens. Google only issues a
# ``refresh_token`` when the authorize request carries
# ``access_type="offline"``; ``prompt="consent"`` forces it to be
# re-issued on every reconnect (Google otherwise returns one only on
# the user's first-ever authorization). The modulex runtime forwards
# these from the manifest ``oauth_config`` into the authorize URL
# (``credentials.py`` ``additional_params``). Leave as ``None`` for
# providers that issue refresh tokens unconditionally.
access_type: str | None = None
prompt: str | None = None
# Whether the provider supports PKCE (RFC 7636) on the auth-code flow.
# The modulex runtime defaults to PKCE on for every provider; a few
# providers (e.g. Netlify) REJECT the token exchange with
# ``invalid_grant`` when an unexpected ``code_verifier`` is present.
# Set ``False`` for those so the runtime omits ``code_challenge`` at
# authorize time and ``code_verifier`` at token time. The runtime reads
# this from the manifest ``oauth_config`` (it currently hardcodes PKCE
# on, so honoring this flag is a small modulex-side change — see
# external brief).
use_pkce: bool = True
class OAuth2AuthSchema(_AuthSchemaBase):
auth_type: Literal["oauth2"] = "oauth2"
oauth_config: OAuthConfig
class BearerTokenAuthSchema(_AuthSchemaBase):
auth_type: Literal["bearer_token"] = "bearer_token"
class ApiKeyAuthSchema(_AuthSchemaBase):
auth_type: Literal["api_key"] = "api_key"
class ModulexKeyAuthSchema(_AuthSchemaBase):
auth_type: Literal["modulex_key"] = "modulex_key"
class CustomAuthSchema(_AuthSchemaBase):
auth_type: Literal["custom"] = "custom"
class InternalAuthSchema(_AuthSchemaBase):
auth_type: Literal["internal"] = "internal"
AuthSchema = Annotated[
OAuth2AuthSchema
| BearerTokenAuthSchema
| ApiKeyAuthSchema
| ModulexKeyAuthSchema
| CustomAuthSchema
| InternalAuthSchema,
Field(discriminator="auth_type"),
]
# --- Top-level manifest ------------------------------------------------
class IntegrationManifest(BaseModel):
"""The contract every integration's ``manifest.py`` produces."""
model_config = ConfigDict(extra="forbid")
integration_type: Literal["tool"] = "tool"
name: str = Field(pattern=r"^[a-z][a-z0-9_]*$")
display_name: str
description: str
version: str = "1.0.0"
author: str = "ModuleX"
logo: str | None = None
app_url: str | None = None
categories: list[str] = Field(default_factory=list)
actions: list[ActionDefinition] = Field(default_factory=list)
auth_schemas: list[AuthSchema] = Field(default_factory=list)