deepquery-sdk 1.0.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.
- deepquery_sdk/__init__.py +101 -0
- deepquery_sdk/action.py +104 -0
- deepquery_sdk/auth/__init__.py +45 -0
- deepquery_sdk/auth/base.py +96 -0
- deepquery_sdk/auth/oauth2.py +106 -0
- deepquery_sdk/auth/strategies.py +91 -0
- deepquery_sdk/classification.py +43 -0
- deepquery_sdk/cli/__init__.py +5 -0
- deepquery_sdk/cli/__main__.py +64 -0
- deepquery_sdk/cli/commands.py +208 -0
- deepquery_sdk/cli/loader.py +95 -0
- deepquery_sdk/compat.py +79 -0
- deepquery_sdk/connector.py +283 -0
- deepquery_sdk/gate.py +83 -0
- deepquery_sdk/harness/__init__.py +5 -0
- deepquery_sdk/harness/mock_agent.py +119 -0
- deepquery_sdk/manifest.py +78 -0
- deepquery_sdk/mcp_emit/__init__.py +5 -0
- deepquery_sdk/mcp_emit/emitter.py +174 -0
- deepquery_sdk/provenance.py +65 -0
- deepquery_sdk/resource.py +65 -0
- deepquery_sdk/templates/connector/.gitignore.tmpl +9 -0
- deepquery_sdk/templates/connector/README.md.tmpl +22 -0
- deepquery_sdk/templates/connector/connector.py.tmpl +74 -0
- deepquery_sdk/templates/connector/pyproject.toml.tmpl +23 -0
- deepquery_sdk/templates/connector/tests/test_connector.py.tmpl +41 -0
- deepquery_sdk/validation.py +201 -0
- deepquery_sdk-1.0.0.dist-info/METADATA +195 -0
- deepquery_sdk-1.0.0.dist-info/RECORD +32 -0
- deepquery_sdk-1.0.0.dist-info/WHEEL +4 -0
- deepquery_sdk-1.0.0.dist-info/entry_points.txt +2 -0
- deepquery_sdk-1.0.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""DeepQuerySDK — the Connector Development Kit for Deep Query.
|
|
2
|
+
|
|
3
|
+
Build a connector by subclassing `Connector` and declaring reads with
|
|
4
|
+
`@resource` and writes with `@action`. The connector emits a standard MCP
|
|
5
|
+
server (see `deepquery_sdk.mcp_emit`).
|
|
6
|
+
|
|
7
|
+
See SDK_GUIDE.md for the full specification.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .action import ActionSpec, action
|
|
13
|
+
from .auth import (
|
|
14
|
+
ApiKeyAuth,
|
|
15
|
+
AppliedAuth,
|
|
16
|
+
AuthStrategy,
|
|
17
|
+
BasicAuth,
|
|
18
|
+
BearerAuth,
|
|
19
|
+
Credential,
|
|
20
|
+
CredentialError,
|
|
21
|
+
CredentialProvider,
|
|
22
|
+
MTLSAuth,
|
|
23
|
+
OAuth2Auth,
|
|
24
|
+
PKCEPair,
|
|
25
|
+
build_authorization_url,
|
|
26
|
+
build_token_request,
|
|
27
|
+
env_credential_provider,
|
|
28
|
+
generate_pkce,
|
|
29
|
+
static_credential_provider,
|
|
30
|
+
)
|
|
31
|
+
from .classification import CapabilityKind
|
|
32
|
+
from .connector import Connector, ConnectorContractError
|
|
33
|
+
from .gate import ActionStatus, ApprovalGate, GateError, PendingAction
|
|
34
|
+
from .compat import (
|
|
35
|
+
CompatibilityResult,
|
|
36
|
+
IncompatibleConnectorError,
|
|
37
|
+
assert_compatible,
|
|
38
|
+
check_manifest_compatibility,
|
|
39
|
+
)
|
|
40
|
+
from .manifest import (
|
|
41
|
+
ActionManifest,
|
|
42
|
+
AuthMethod,
|
|
43
|
+
DeploymentCompat,
|
|
44
|
+
Manifest,
|
|
45
|
+
ResourceManifest,
|
|
46
|
+
SDK_MAJOR_VERSION,
|
|
47
|
+
SDK_VERSION,
|
|
48
|
+
)
|
|
49
|
+
from .provenance import CitedRecord, Provenance, make_provenance, now_iso
|
|
50
|
+
from .resource import ResourceSpec, resource
|
|
51
|
+
|
|
52
|
+
__version__ = SDK_VERSION
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"Connector",
|
|
56
|
+
"ConnectorContractError",
|
|
57
|
+
"resource",
|
|
58
|
+
"ResourceSpec",
|
|
59
|
+
"action",
|
|
60
|
+
"ActionSpec",
|
|
61
|
+
"CapabilityKind",
|
|
62
|
+
"CitedRecord",
|
|
63
|
+
"Provenance",
|
|
64
|
+
"make_provenance",
|
|
65
|
+
"now_iso",
|
|
66
|
+
"Manifest",
|
|
67
|
+
"ResourceManifest",
|
|
68
|
+
"ActionManifest",
|
|
69
|
+
"AuthMethod",
|
|
70
|
+
"DeploymentCompat",
|
|
71
|
+
"SDK_MAJOR_VERSION",
|
|
72
|
+
"SDK_VERSION",
|
|
73
|
+
# compatibility
|
|
74
|
+
"assert_compatible",
|
|
75
|
+
"check_manifest_compatibility",
|
|
76
|
+
"CompatibilityResult",
|
|
77
|
+
"IncompatibleConnectorError",
|
|
78
|
+
# auth
|
|
79
|
+
"AuthStrategy",
|
|
80
|
+
"Credential",
|
|
81
|
+
"CredentialError",
|
|
82
|
+
"CredentialProvider",
|
|
83
|
+
"AppliedAuth",
|
|
84
|
+
"ApiKeyAuth",
|
|
85
|
+
"BasicAuth",
|
|
86
|
+
"BearerAuth",
|
|
87
|
+
"OAuth2Auth",
|
|
88
|
+
"MTLSAuth",
|
|
89
|
+
"PKCEPair",
|
|
90
|
+
"generate_pkce",
|
|
91
|
+
"build_authorization_url",
|
|
92
|
+
"build_token_request",
|
|
93
|
+
"static_credential_provider",
|
|
94
|
+
"env_credential_provider",
|
|
95
|
+
# gate
|
|
96
|
+
"ApprovalGate",
|
|
97
|
+
"ActionStatus",
|
|
98
|
+
"PendingAction",
|
|
99
|
+
"GateError",
|
|
100
|
+
"__version__",
|
|
101
|
+
]
|
deepquery_sdk/action.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Action definitions — the write/mutation side of a connector.
|
|
2
|
+
|
|
3
|
+
An action is a state-mutating capability. It is declared with the `@action`
|
|
4
|
+
decorator on a `Connector` method (the *execute* step), and its mandatory
|
|
5
|
+
*preview* is attached with `@<action>.preview`. The separation of preview and
|
|
6
|
+
execute is the architectural heart of the approval-gate safety model: the agent
|
|
7
|
+
and the gate always see the preview first, and execute is only ever called after
|
|
8
|
+
human confirmation.
|
|
9
|
+
|
|
10
|
+
In Phase 1 the SDK captures and validates this contract and emits the action
|
|
11
|
+
into the MCP server tagged `dq.mutates: true`. The full gated
|
|
12
|
+
`preview → approve → execute` drive is wired through the harness in Phase 2; an
|
|
13
|
+
emitted action invoked directly returns its preview and does **not** execute.
|
|
14
|
+
|
|
15
|
+
See SDK_GUIDE.md §4.3 and §5.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Callable
|
|
21
|
+
|
|
22
|
+
from .classification import CapabilityKind
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ActionSpec:
|
|
26
|
+
"""Declarative metadata + bound functions for one action.
|
|
27
|
+
|
|
28
|
+
Becomes the class attribute in place of the decorated method, so
|
|
29
|
+
`@<action>.preview` can register the preview step at class-body time.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
kind = CapabilityKind.ACTION
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
name: str | None,
|
|
38
|
+
description: str,
|
|
39
|
+
input_schema: dict[str, Any],
|
|
40
|
+
) -> None:
|
|
41
|
+
self._name = name
|
|
42
|
+
self.description = description
|
|
43
|
+
self.input_schema = input_schema
|
|
44
|
+
self.execute_fn: Callable | None = None
|
|
45
|
+
self.preview_fn: Callable | None = None
|
|
46
|
+
self.attr_name: str | None = None
|
|
47
|
+
|
|
48
|
+
# -- decorator wiring -------------------------------------------------
|
|
49
|
+
def __call__(self, fn: Callable) -> "ActionSpec":
|
|
50
|
+
"""Capture the execute function (the body of `@action(...)`)."""
|
|
51
|
+
self.execute_fn = fn
|
|
52
|
+
if self._name is None:
|
|
53
|
+
self._name = fn.__name__
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def preview(self, fn: Callable) -> Callable:
|
|
57
|
+
"""Register the mandatory preview function.
|
|
58
|
+
|
|
59
|
+
Usage::
|
|
60
|
+
|
|
61
|
+
@create_issue.preview
|
|
62
|
+
def _(self, **params) -> str: ...
|
|
63
|
+
|
|
64
|
+
Returns the preview function unchanged so it does not shadow the action
|
|
65
|
+
attribute on the class.
|
|
66
|
+
"""
|
|
67
|
+
self.preview_fn = fn
|
|
68
|
+
return fn
|
|
69
|
+
|
|
70
|
+
def __set_name__(self, owner: type, attr_name: str) -> None:
|
|
71
|
+
self.attr_name = attr_name
|
|
72
|
+
|
|
73
|
+
# -- accessors --------------------------------------------------------
|
|
74
|
+
@property
|
|
75
|
+
def name(self) -> str:
|
|
76
|
+
# _name is set either explicitly or from the execute fn in __call__.
|
|
77
|
+
assert self._name is not None, "action defined without an execute function"
|
|
78
|
+
return self._name
|
|
79
|
+
|
|
80
|
+
def has_preview(self) -> bool:
|
|
81
|
+
return self.preview_fn is not None
|
|
82
|
+
|
|
83
|
+
def __repr__(self) -> str: # pragma: no cover - debug aid
|
|
84
|
+
return f"ActionSpec(name={self._name!r}, has_preview={self.has_preview()})"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def action(
|
|
88
|
+
*,
|
|
89
|
+
description: str,
|
|
90
|
+
name: str | None = None,
|
|
91
|
+
input_schema: dict[str, Any] | None = None,
|
|
92
|
+
) -> ActionSpec:
|
|
93
|
+
"""Declare a state-mutating action.
|
|
94
|
+
|
|
95
|
+
Decorate the *execute* method with `@action(...)`, then attach the mandatory
|
|
96
|
+
preview with `@<action>.preview`. A connector that declares an action without
|
|
97
|
+
a preview is a contract violation and is rejected when the connector class is
|
|
98
|
+
defined.
|
|
99
|
+
"""
|
|
100
|
+
return ActionSpec(
|
|
101
|
+
name=name,
|
|
102
|
+
description=description,
|
|
103
|
+
input_schema=input_schema or {"type": "object", "properties": {}},
|
|
104
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Authentication scaffolding for connectors.
|
|
2
|
+
|
|
3
|
+
Connectors declare an `AuthStrategy` and read an injected `Credential` per call;
|
|
4
|
+
they never store credentials. See SDK_GUIDE.md §7.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base import (
|
|
8
|
+
AppliedAuth,
|
|
9
|
+
AuthStrategy,
|
|
10
|
+
Credential,
|
|
11
|
+
CredentialError,
|
|
12
|
+
CredentialProvider,
|
|
13
|
+
env_credential_provider,
|
|
14
|
+
static_credential_provider,
|
|
15
|
+
)
|
|
16
|
+
from .oauth2 import (
|
|
17
|
+
PKCEPair,
|
|
18
|
+
TokenRequest,
|
|
19
|
+
build_authorization_url,
|
|
20
|
+
build_refresh_request,
|
|
21
|
+
build_token_request,
|
|
22
|
+
generate_pkce,
|
|
23
|
+
)
|
|
24
|
+
from .strategies import ApiKeyAuth, BasicAuth, BearerAuth, MTLSAuth, OAuth2Auth
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"AppliedAuth",
|
|
28
|
+
"AuthStrategy",
|
|
29
|
+
"Credential",
|
|
30
|
+
"CredentialError",
|
|
31
|
+
"CredentialProvider",
|
|
32
|
+
"static_credential_provider",
|
|
33
|
+
"env_credential_provider",
|
|
34
|
+
"ApiKeyAuth",
|
|
35
|
+
"BasicAuth",
|
|
36
|
+
"BearerAuth",
|
|
37
|
+
"OAuth2Auth",
|
|
38
|
+
"MTLSAuth",
|
|
39
|
+
"PKCEPair",
|
|
40
|
+
"generate_pkce",
|
|
41
|
+
"build_authorization_url",
|
|
42
|
+
"build_token_request",
|
|
43
|
+
"build_refresh_request",
|
|
44
|
+
"TokenRequest",
|
|
45
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Auth foundations: credentials, strategies, and the injection hook.
|
|
2
|
+
|
|
3
|
+
Connectors authenticate to external systems but **never store credentials**
|
|
4
|
+
(SDK_GUIDE.md §7). The gateway owns credential lifecycle and injects a valid
|
|
5
|
+
credential at call time through a `CredentialProvider`. An `AuthStrategy`
|
|
6
|
+
declares *how* to attach that credential to outbound requests; it never holds the
|
|
7
|
+
secret itself — it receives one per call and returns the headers/cert to apply.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import Any, Callable, Optional
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from ..manifest import AuthMethod
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Credential(BaseModel):
|
|
21
|
+
"""Opaque secret material injected by the gateway for a single call.
|
|
22
|
+
|
|
23
|
+
Only the fields relevant to the connector's declared auth method are set.
|
|
24
|
+
The connector reads this via `self.current_credential` during a call and must
|
|
25
|
+
never persist it.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
token: Optional[str] = None # OAuth bearer token or API key value
|
|
29
|
+
username: Optional[str] = None
|
|
30
|
+
password: Optional[str] = None
|
|
31
|
+
client_cert_path: Optional[str] = None
|
|
32
|
+
client_key_path: Optional[str] = None
|
|
33
|
+
extra: dict[str, Any] = Field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AppliedAuth(BaseModel):
|
|
37
|
+
"""The result of applying a strategy to a credential: what to attach to a request."""
|
|
38
|
+
|
|
39
|
+
headers: dict[str, str] = Field(default_factory=dict)
|
|
40
|
+
# httpx-style client cert: a (cert, key) path tuple, when using mTLS.
|
|
41
|
+
cert: Optional[tuple[str, str]] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CredentialError(Exception):
|
|
45
|
+
"""Raised when a credential is required for a call but none was injected."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# The gateway supplies one of these. It is called per request and may return a
|
|
49
|
+
# different (e.g. per-user) credential each time. Returning None means "no
|
|
50
|
+
# credential available" — strategies that require one will raise.
|
|
51
|
+
CredentialProvider = Callable[[], Optional[Credential]]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AuthStrategy(ABC):
|
|
55
|
+
"""Declares how a connector attaches an injected credential to its requests."""
|
|
56
|
+
|
|
57
|
+
method: AuthMethod
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def apply(self, credential: Credential) -> AppliedAuth:
|
|
61
|
+
"""Return the headers / cert to attach, given an injected credential."""
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def scopes(self) -> list[str]:
|
|
66
|
+
"""OAuth scopes this strategy requests (empty for non-OAuth)."""
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def static_credential_provider(credential: Credential) -> CredentialProvider:
|
|
71
|
+
"""A provider that always returns the same credential.
|
|
72
|
+
|
|
73
|
+
Used for air-gapped / static injection (API keys, mTLS certs) sourced from
|
|
74
|
+
the institution's own secret store with no external OAuth round-trip
|
|
75
|
+
(SDK_GUIDE.md §7).
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def _provider() -> Credential:
|
|
79
|
+
return credential
|
|
80
|
+
|
|
81
|
+
return _provider
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def env_credential_provider(*, token_env: str) -> CredentialProvider:
|
|
85
|
+
"""A provider that reads a bearer token / API key from an environment variable.
|
|
86
|
+
|
|
87
|
+
Convenient for self-hosted, air-gapped connectors whose secret store exposes
|
|
88
|
+
credentials through the process environment.
|
|
89
|
+
"""
|
|
90
|
+
import os
|
|
91
|
+
|
|
92
|
+
def _provider() -> Optional[Credential]:
|
|
93
|
+
value = os.environ.get(token_env)
|
|
94
|
+
return Credential(token=value) if value else None
|
|
95
|
+
|
|
96
|
+
return _provider
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""OAuth 2.1 flow-construction scaffolding (authorization code + PKCE).
|
|
2
|
+
|
|
3
|
+
These are the pure building blocks a gateway uses to run the authorization-code
|
|
4
|
+
flow with PKCE: generate a PKCE pair, build the authorization URL, and build the
|
|
5
|
+
token-exchange request. The SDK intentionally does **not** perform token storage,
|
|
6
|
+
exchange-over-the-wire, or refresh — credential lifecycle is owned by the gateway
|
|
7
|
+
(SDK_GUIDE.md §7). Keeping these helpers pure also makes them testable offline.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import hashlib
|
|
14
|
+
import secrets
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from urllib.parse import urlencode
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _b64url(data: bytes) -> str:
|
|
20
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class PKCEPair:
|
|
25
|
+
"""A PKCE code verifier and its S256-derived challenge."""
|
|
26
|
+
|
|
27
|
+
verifier: str
|
|
28
|
+
challenge: str
|
|
29
|
+
method: str = "S256"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def generate_pkce() -> PKCEPair:
|
|
33
|
+
"""Generate a PKCE verifier/challenge pair (RFC 7636, S256)."""
|
|
34
|
+
verifier = _b64url(secrets.token_bytes(32)) # 43-char high-entropy verifier
|
|
35
|
+
challenge = _b64url(hashlib.sha256(verifier.encode("ascii")).digest())
|
|
36
|
+
return PKCEPair(verifier=verifier, challenge=challenge)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_authorization_url(
|
|
40
|
+
*,
|
|
41
|
+
authorize_endpoint: str,
|
|
42
|
+
client_id: str,
|
|
43
|
+
redirect_uri: str,
|
|
44
|
+
scopes: list[str],
|
|
45
|
+
code_challenge: str,
|
|
46
|
+
state: str | None = None,
|
|
47
|
+
code_challenge_method: str = "S256",
|
|
48
|
+
) -> str:
|
|
49
|
+
"""Build the authorization-request URL the user is redirected to."""
|
|
50
|
+
params = {
|
|
51
|
+
"response_type": "code",
|
|
52
|
+
"client_id": client_id,
|
|
53
|
+
"redirect_uri": redirect_uri,
|
|
54
|
+
"scope": " ".join(scopes),
|
|
55
|
+
"code_challenge": code_challenge,
|
|
56
|
+
"code_challenge_method": code_challenge_method,
|
|
57
|
+
}
|
|
58
|
+
if state is not None:
|
|
59
|
+
params["state"] = state
|
|
60
|
+
return f"{authorize_endpoint}?{urlencode(params)}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class TokenRequest:
|
|
65
|
+
"""A ready-to-send token-exchange request (the gateway performs the POST)."""
|
|
66
|
+
|
|
67
|
+
url: str
|
|
68
|
+
data: dict[str, str]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_token_request(
|
|
72
|
+
*,
|
|
73
|
+
token_endpoint: str,
|
|
74
|
+
client_id: str,
|
|
75
|
+
code: str,
|
|
76
|
+
code_verifier: str,
|
|
77
|
+
redirect_uri: str,
|
|
78
|
+
) -> TokenRequest:
|
|
79
|
+
"""Build the authorization-code → token exchange request body."""
|
|
80
|
+
return TokenRequest(
|
|
81
|
+
url=token_endpoint,
|
|
82
|
+
data={
|
|
83
|
+
"grant_type": "authorization_code",
|
|
84
|
+
"client_id": client_id,
|
|
85
|
+
"code": code,
|
|
86
|
+
"code_verifier": code_verifier,
|
|
87
|
+
"redirect_uri": redirect_uri,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_refresh_request(
|
|
93
|
+
*,
|
|
94
|
+
token_endpoint: str,
|
|
95
|
+
client_id: str,
|
|
96
|
+
refresh_token: str,
|
|
97
|
+
) -> TokenRequest:
|
|
98
|
+
"""Build a refresh-token request (gateway performs and stores the result)."""
|
|
99
|
+
return TokenRequest(
|
|
100
|
+
url=token_endpoint,
|
|
101
|
+
data={
|
|
102
|
+
"grant_type": "refresh_token",
|
|
103
|
+
"client_id": client_id,
|
|
104
|
+
"refresh_token": refresh_token,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Concrete auth strategies: API key, basic, bearer/OAuth2, mTLS.
|
|
2
|
+
|
|
3
|
+
Each strategy receives an injected `Credential` per call and returns the headers
|
|
4
|
+
(or client cert) to attach. None of them store the credential.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
|
|
11
|
+
from ..manifest import AuthMethod
|
|
12
|
+
from .base import AppliedAuth, AuthStrategy, Credential, CredentialError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApiKeyAuth(AuthStrategy):
|
|
16
|
+
"""Attach an API key as a header, e.g. `Authorization: Bearer <key>` or `X-API-Key: <key>`."""
|
|
17
|
+
|
|
18
|
+
method = AuthMethod.API_KEY
|
|
19
|
+
|
|
20
|
+
def __init__(self, *, header_name: str = "Authorization", value_prefix: str = "") -> None:
|
|
21
|
+
self.header_name = header_name
|
|
22
|
+
self.value_prefix = value_prefix
|
|
23
|
+
|
|
24
|
+
def apply(self, credential: Credential) -> AppliedAuth:
|
|
25
|
+
if not credential.token:
|
|
26
|
+
raise CredentialError("ApiKeyAuth requires credential.token")
|
|
27
|
+
value = f"{self.value_prefix}{credential.token}" if self.value_prefix else credential.token
|
|
28
|
+
return AppliedAuth(headers={self.header_name: value})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BasicAuth(AuthStrategy):
|
|
32
|
+
"""HTTP Basic auth from an injected username/password."""
|
|
33
|
+
|
|
34
|
+
method = AuthMethod.BASIC
|
|
35
|
+
|
|
36
|
+
def apply(self, credential: Credential) -> AppliedAuth:
|
|
37
|
+
if credential.username is None or credential.password is None:
|
|
38
|
+
raise CredentialError("BasicAuth requires credential.username and credential.password")
|
|
39
|
+
raw = f"{credential.username}:{credential.password}".encode()
|
|
40
|
+
encoded = base64.b64encode(raw).decode()
|
|
41
|
+
return AppliedAuth(headers={"Authorization": f"Basic {encoded}"})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BearerAuth(AuthStrategy):
|
|
45
|
+
"""Attach a bearer token (the shape an OAuth access token takes on requests)."""
|
|
46
|
+
|
|
47
|
+
method = AuthMethod.OAUTH2
|
|
48
|
+
|
|
49
|
+
def apply(self, credential: Credential) -> AppliedAuth:
|
|
50
|
+
if not credential.token:
|
|
51
|
+
raise CredentialError("BearerAuth requires credential.token")
|
|
52
|
+
return AppliedAuth(headers={"Authorization": f"Bearer {credential.token}"})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class OAuth2Auth(BearerAuth):
|
|
56
|
+
"""OAuth 2.1 strategy: declares endpoints + scopes, attaches the bearer token.
|
|
57
|
+
|
|
58
|
+
The connector declares the flow's endpoints and the minimum scopes it needs
|
|
59
|
+
(least-privilege, SDK_GUIDE.md §13). The gateway runs and stores the grant
|
|
60
|
+
(§7); see `deepquery_sdk.auth.oauth2` for the PKCE flow-construction helpers.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
method = AuthMethod.OAUTH2
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
authorize_endpoint: str,
|
|
69
|
+
token_endpoint: str,
|
|
70
|
+
scopes: list[str] | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
self.authorize_endpoint = authorize_endpoint
|
|
73
|
+
self.token_endpoint = token_endpoint
|
|
74
|
+
self._scopes = list(scopes or [])
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def scopes(self) -> list[str]:
|
|
78
|
+
return self._scopes
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MTLSAuth(AuthStrategy):
|
|
82
|
+
"""Mutual TLS: attach an injected client certificate/key pair."""
|
|
83
|
+
|
|
84
|
+
method = AuthMethod.MTLS
|
|
85
|
+
|
|
86
|
+
def apply(self, credential: Credential) -> AppliedAuth:
|
|
87
|
+
if not credential.client_cert_path or not credential.client_key_path:
|
|
88
|
+
raise CredentialError(
|
|
89
|
+
"MTLSAuth requires credential.client_cert_path and credential.client_key_path"
|
|
90
|
+
)
|
|
91
|
+
return AppliedAuth(cert=(credential.client_cert_path, credential.client_key_path))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Read / Action classification — the safety contract.
|
|
2
|
+
|
|
3
|
+
Every capability a connector exposes is classified *at definition time* as
|
|
4
|
+
either a read (Resource) or an action (mutating tool). This classification is
|
|
5
|
+
declared by the developer, never inferred at runtime, and is embedded in the
|
|
6
|
+
emitted MCP server as metadata the Connector Infrastructure layer reads to
|
|
7
|
+
decide gating.
|
|
8
|
+
|
|
9
|
+
See SDK_GUIDE.md §5.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CapabilityKind(str, Enum):
|
|
18
|
+
"""How a capability is classified for the approval gate."""
|
|
19
|
+
|
|
20
|
+
RESOURCE = "resource" # read-only, ungated, citeable
|
|
21
|
+
ACTION = "action" # mutates external state, gated behind human approval
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Metadata keys attached to the emitted MCP `Tool._meta`. The Connector
|
|
25
|
+
# Infrastructure gateway reads these to decide gating and citation handling.
|
|
26
|
+
# Namespaced under `dq.` so they never collide with other MCP metadata.
|
|
27
|
+
DQ_MUTATES = "dq.mutates" # bool — True for actions, False for resources
|
|
28
|
+
DQ_KIND = "dq.kind" # CapabilityKind value
|
|
29
|
+
DQ_CONNECTOR = "dq.connector" # originating connector name
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def mutates(kind: CapabilityKind) -> bool:
|
|
33
|
+
"""Whether a capability of this kind mutates external state."""
|
|
34
|
+
return kind is CapabilityKind.ACTION
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def meta_for(kind: CapabilityKind, connector_name: str) -> dict[str, object]:
|
|
38
|
+
"""Build the `dq.*` metadata block for a capability's emitted MCP tool."""
|
|
39
|
+
return {
|
|
40
|
+
DQ_KIND: kind.value,
|
|
41
|
+
DQ_MUTATES: mutates(kind),
|
|
42
|
+
DQ_CONNECTOR: connector_name,
|
|
43
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""The `deepquery` command-line entry point.
|
|
2
|
+
|
|
3
|
+
Built on stdlib argparse — no third-party CLI dependency — so the SDK stays a
|
|
4
|
+
thin install (mcp + pydantic).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from . import commands
|
|
13
|
+
|
|
14
|
+
_TARGET_HELP = (
|
|
15
|
+
"connector target: 'module.path:ClassName', a module, or a path to a "
|
|
16
|
+
"connector .py file (or a directory containing connector.py)"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
21
|
+
parser = argparse.ArgumentParser(prog="deepquery", description="DeepQuerySDK connector tooling.")
|
|
22
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
23
|
+
|
|
24
|
+
p_scaffold = sub.add_parser("scaffold", help="generate a new connector from the template")
|
|
25
|
+
p_scaffold.add_argument("name", help="connector name, e.g. 'jira' or 'acme-crm'")
|
|
26
|
+
p_scaffold.add_argument("--dir", help="output directory (default: ./<name>)")
|
|
27
|
+
p_scaffold.set_defaults(func=commands.cmd_scaffold)
|
|
28
|
+
|
|
29
|
+
p_validate = sub.add_parser("validate", help="static-check a connector against the safety contracts")
|
|
30
|
+
p_validate.add_argument("target", help=_TARGET_HELP)
|
|
31
|
+
p_validate.set_defaults(func=commands.cmd_validate)
|
|
32
|
+
|
|
33
|
+
p_manifest = sub.add_parser("manifest", help="print or export the connector manifest")
|
|
34
|
+
p_manifest.add_argument("target", help=_TARGET_HELP)
|
|
35
|
+
p_manifest.add_argument("--out", help="write manifest JSON to this file instead of stdout")
|
|
36
|
+
p_manifest.set_defaults(func=commands.cmd_manifest)
|
|
37
|
+
|
|
38
|
+
p_emit = sub.add_parser("emit", help="produce the deployable MCP server artifact")
|
|
39
|
+
p_emit.add_argument("target", help=_TARGET_HELP)
|
|
40
|
+
p_emit.add_argument("--out", default="dist", help="output directory (default: ./dist)")
|
|
41
|
+
p_emit.set_defaults(func=commands.cmd_emit)
|
|
42
|
+
|
|
43
|
+
p_run = sub.add_parser("run-dev", help="drive the connector locally with the mock agent")
|
|
44
|
+
p_run.add_argument("target", help=_TARGET_HELP)
|
|
45
|
+
p_run.set_defaults(func=commands.cmd_run_dev)
|
|
46
|
+
|
|
47
|
+
return parser
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(argv: list[str] | None = None) -> int:
|
|
51
|
+
# The guide references sections with '§' and uses em dashes; make sure those
|
|
52
|
+
# render on legacy Windows consoles (cp1252) rather than showing as '�'.
|
|
53
|
+
for stream in (sys.stdout, sys.stderr):
|
|
54
|
+
try:
|
|
55
|
+
stream.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
|
|
56
|
+
except (AttributeError, ValueError):
|
|
57
|
+
pass
|
|
58
|
+
parser = build_parser()
|
|
59
|
+
args = parser.parse_args(argv)
|
|
60
|
+
return args.func(args)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
sys.exit(main())
|