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,74 @@
|
|
|
1
|
+
"""__CONNECTOR_TITLE__ connector for Deep Query, built with DeepQuerySDK.
|
|
2
|
+
|
|
3
|
+
Fill in the auth endpoints, the resource fetch, and the action preview/execute
|
|
4
|
+
with real calls to your system. Run `deepquery validate <this file>` to check the
|
|
5
|
+
safety contracts, and `deepquery run-dev <this file>` to exercise it locally.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from deepquery_sdk import Connector, OAuth2Auth, action, resource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class __CLASS_NAME__(Connector):
|
|
14
|
+
name = "__CONNECTOR_NAME__"
|
|
15
|
+
version = "0.1.0"
|
|
16
|
+
description = "TODO: one line describing what this connector reads and does."
|
|
17
|
+
|
|
18
|
+
# Declare how the connector authenticates. Swap for ApiKeyAuth / BasicAuth /
|
|
19
|
+
# MTLSAuth as needed, and request the *minimum* scopes (§13). The gateway runs
|
|
20
|
+
# and stores the grant; this connector never stores credentials.
|
|
21
|
+
auth = OAuth2Auth(
|
|
22
|
+
authorize_endpoint="https://example.com/oauth/authorize",
|
|
23
|
+
token_endpoint="https://example.com/oauth/token",
|
|
24
|
+
scopes=["read"],
|
|
25
|
+
)
|
|
26
|
+
requires_network = True
|
|
27
|
+
air_gapped_capable = False
|
|
28
|
+
|
|
29
|
+
# -- a read (resource): returns citeable records ----------------------
|
|
30
|
+
@resource(
|
|
31
|
+
description="TODO: describe what this read returns and when to use it.",
|
|
32
|
+
input_schema={
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {"query": {"type": "string", "description": "Search text."}},
|
|
35
|
+
"required": ["query"],
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
def search(self, query: str):
|
|
39
|
+
# TODO: call your system's read API. Wrap each record with self.cite(...)
|
|
40
|
+
# so it carries a provenance envelope (§6).
|
|
41
|
+
return [
|
|
42
|
+
self.cite(
|
|
43
|
+
{"id": "EXAMPLE-1", "title": f"Result for {query}"},
|
|
44
|
+
source_object_id="EXAMPLE-1",
|
|
45
|
+
title_or_label=f"EXAMPLE-1 — Result for {query}",
|
|
46
|
+
deep_link="https://example.com/objects/EXAMPLE-1",
|
|
47
|
+
mutability_note="live data — may change after retrieval",
|
|
48
|
+
)
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# -- an action: gated behind preview -> approve -> execute ------------
|
|
52
|
+
@action(
|
|
53
|
+
description="TODO: describe what this action changes.",
|
|
54
|
+
input_schema={
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {"target": {"type": "string"}, "value": {"type": "string"}},
|
|
57
|
+
"required": ["target", "value"],
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
def do_thing(self, target: str, value: str):
|
|
61
|
+
# Only ever called after the approval gate confirms the preview.
|
|
62
|
+
auth = self.apply_auth() # headers built from the injected credential
|
|
63
|
+
return {"updated": target, "value": value}
|
|
64
|
+
|
|
65
|
+
@do_thing.preview
|
|
66
|
+
def _(self, target: str, value: str) -> str:
|
|
67
|
+
# Describe exactly what execute will do, without doing it (§4.3).
|
|
68
|
+
return f"Will set '{target}' to '{value}'."
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
from deepquery_sdk.mcp_emit import run_stdio
|
|
73
|
+
|
|
74
|
+
run_stdio(__CLASS_NAME__())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "__CONNECTOR_NAME__-connector"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A Deep Query connector for __CONNECTOR_TITLE__."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
# Targets DeepQuerySDK major 1. A major bump means a fresh review (§11).
|
|
13
|
+
"deepquery-sdk>=1,<2",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.25"]
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
only-include = ["connector.py"]
|
|
21
|
+
|
|
22
|
+
[tool.pytest.ini_options]
|
|
23
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Local tests for the __CONNECTOR_TITLE__ connector.
|
|
2
|
+
|
|
3
|
+
These drive the connector through the mock agent over a real in-memory MCP
|
|
4
|
+
session — the same way Deep Query's Agent Layer will. Run with: pytest
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from deepquery_sdk import Credential, static_credential_provider
|
|
10
|
+
from deepquery_sdk.harness import MockAgent
|
|
11
|
+
from deepquery_sdk.validation import validate_connector
|
|
12
|
+
|
|
13
|
+
from connector import __CLASS_NAME__
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _connector() -> __CLASS_NAME__:
|
|
17
|
+
c = __CLASS_NAME__()
|
|
18
|
+
# Stand in for the gateway with a test credential so actions can execute.
|
|
19
|
+
c.set_credential_provider(static_credential_provider(Credential(token="test-token")))
|
|
20
|
+
return c
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_connector_validates_clean():
|
|
24
|
+
report = validate_connector(_connector())
|
|
25
|
+
assert report.ok, report.format()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def test_read_returns_provenance():
|
|
29
|
+
async with MockAgent(_connector()) as agent:
|
|
30
|
+
records = await agent.read("search", query="hello")
|
|
31
|
+
assert records, "expected at least one record"
|
|
32
|
+
assert records[0]["provenance"]["connector_name"] == "__CONNECTOR_NAME__"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def test_action_preview_approve_and_reject():
|
|
36
|
+
async with MockAgent(_connector()) as agent:
|
|
37
|
+
approved = await agent.run_action("do_thing", approve=True, target="x", value="y")
|
|
38
|
+
assert approved["outcome"]["status"] == "executed"
|
|
39
|
+
|
|
40
|
+
rejected = await agent.run_action("do_thing", approve=False, target="x", value="y")
|
|
41
|
+
assert rejected["outcome"]["status"] == "rejected"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Static + structural validation — the `validate` gatekeeper.
|
|
2
|
+
|
|
3
|
+
Encodes the safety contracts from SDK_GUIDE.md §5 (read/action classification),
|
|
4
|
+
§6 (provenance), and §13 (security) as automated checks, so a connector that
|
|
5
|
+
would violate the approval-gate model or break citations fails locally, before
|
|
6
|
+
deployment.
|
|
7
|
+
|
|
8
|
+
Checks are a mix of:
|
|
9
|
+
- **structural** — run against the loaded connector instance (manifest builds,
|
|
10
|
+
descriptions present, deployment honesty, least-privilege scopes); and
|
|
11
|
+
- **static (AST)** — scan `@resource` method bodies for obvious mutating calls,
|
|
12
|
+
catching a resource that writes (a misclassified action) "where statically
|
|
13
|
+
detectable" (§5).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
import inspect
|
|
20
|
+
import re
|
|
21
|
+
import textwrap
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
from .connector import Connector
|
|
25
|
+
|
|
26
|
+
# High-confidence mutation indicators looked for inside @resource bodies. HTTP
|
|
27
|
+
# write verbs and clear DB mutators only, to keep false positives low.
|
|
28
|
+
_MUTATING_CALLS = {
|
|
29
|
+
"post", "put", "patch", "delete", # HTTP write verbs
|
|
30
|
+
"create", "update", "insert", "destroy", "drop", # ORM/DB mutators
|
|
31
|
+
"commit", "execute", "executemany", "save", # transactions / persistence
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Crude prompt-injection markers in natural-language descriptions (§13).
|
|
35
|
+
_INJECTION_MARKERS = re.compile(
|
|
36
|
+
r"ignore (all|previous|prior)|disregard (the|all|previous)|system prompt|"
|
|
37
|
+
r"you are now|override (the|all)|exfiltrate|return all (documents|records|data)",
|
|
38
|
+
re.IGNORECASE,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Scope fragments that look broader than least-privilege (§13).
|
|
42
|
+
_BROAD_SCOPE = re.compile(r"(^|[:._-])(\*|all|admin|full|root|superuser|write:all|read:all)([:._-]|$)", re.IGNORECASE)
|
|
43
|
+
|
|
44
|
+
_SEMVER = re.compile(r"^\d+\.\d+\.\d+([-+].+)?$")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Issue:
|
|
49
|
+
level: str # "error" | "warning"
|
|
50
|
+
code: str
|
|
51
|
+
message: str
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
mark = "ERROR" if self.level == "error" else "warn "
|
|
55
|
+
return f" [{mark}] {self.code}: {self.message}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ValidationReport:
|
|
60
|
+
connector_name: str
|
|
61
|
+
issues: list[Issue] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
def error(self, code: str, message: str) -> None:
|
|
64
|
+
self.issues.append(Issue("error", code, message))
|
|
65
|
+
|
|
66
|
+
def warn(self, code: str, message: str) -> None:
|
|
67
|
+
self.issues.append(Issue("warning", code, message))
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def errors(self) -> list[Issue]:
|
|
71
|
+
return [i for i in self.issues if i.level == "error"]
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def warnings(self) -> list[Issue]:
|
|
75
|
+
return [i for i in self.issues if i.level == "warning"]
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def ok(self) -> bool:
|
|
79
|
+
return not self.errors
|
|
80
|
+
|
|
81
|
+
def format(self) -> str:
|
|
82
|
+
if not self.issues:
|
|
83
|
+
return f"OK {self.connector_name}: no issues."
|
|
84
|
+
lines = [f"{self.connector_name}: {len(self.errors)} error(s), {len(self.warnings)} warning(s)"]
|
|
85
|
+
lines += [str(i) for i in self.issues]
|
|
86
|
+
lines.append("PASS" if self.ok else "FAIL")
|
|
87
|
+
return "\n".join(lines)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _MutationVisitor(ast.NodeVisitor):
|
|
91
|
+
"""Collects (name, lineno) for calls that look like state mutations."""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
self.found: list[tuple[str, int]] = []
|
|
95
|
+
|
|
96
|
+
def visit_Call(self, node: ast.Call) -> None: # noqa: N802 (ast API)
|
|
97
|
+
func = node.func
|
|
98
|
+
name = None
|
|
99
|
+
if isinstance(func, ast.Attribute):
|
|
100
|
+
name = func.attr
|
|
101
|
+
elif isinstance(func, ast.Name):
|
|
102
|
+
name = func.id
|
|
103
|
+
if name in _MUTATING_CALLS:
|
|
104
|
+
self.found.append((name, getattr(node, "lineno", 0)))
|
|
105
|
+
self.generic_visit(node)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _check_descriptions(connector: Connector, report: ValidationReport) -> None:
|
|
109
|
+
for spec in list(connector.resources) + list(connector.actions):
|
|
110
|
+
if not (spec.description or "").strip():
|
|
111
|
+
report.error("DESC_MISSING", f"capability '{spec.name}' has an empty description (§4).")
|
|
112
|
+
elif _INJECTION_MARKERS.search(spec.description):
|
|
113
|
+
report.warn(
|
|
114
|
+
"DESC_INJECTION",
|
|
115
|
+
f"description for '{spec.name}' contains text resembling a prompt-injection "
|
|
116
|
+
f"instruction; descriptions are read by the planner (§13).",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _check_actions(connector: Connector, report: ValidationReport) -> None:
|
|
121
|
+
for spec in connector.actions:
|
|
122
|
+
# Structural (also enforced at import time, re-checked for a clear message).
|
|
123
|
+
if not spec.has_preview():
|
|
124
|
+
report.error(
|
|
125
|
+
"ACTION_NO_PREVIEW",
|
|
126
|
+
f"action '{spec.name}' has no preview; every action must declare one (§4.3).",
|
|
127
|
+
)
|
|
128
|
+
if spec.execute_fn is None:
|
|
129
|
+
report.error("ACTION_NO_EXECUTE", f"action '{spec.name}' has no execute function.")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _check_resources_are_readonly(connector: Connector, report: ValidationReport) -> None:
|
|
133
|
+
cls = type(connector)
|
|
134
|
+
for spec in connector.resources:
|
|
135
|
+
fn = getattr(cls, spec.attr_name, None)
|
|
136
|
+
if fn is None:
|
|
137
|
+
continue
|
|
138
|
+
try:
|
|
139
|
+
src = textwrap.dedent(inspect.getsource(fn))
|
|
140
|
+
tree = ast.parse(src)
|
|
141
|
+
except (OSError, TypeError, SyntaxError):
|
|
142
|
+
report.warn(
|
|
143
|
+
"RESOURCE_SOURCE_UNAVAILABLE",
|
|
144
|
+
f"could not read source of resource '{spec.name}' for static mutation analysis.",
|
|
145
|
+
)
|
|
146
|
+
continue
|
|
147
|
+
visitor = _MutationVisitor()
|
|
148
|
+
visitor.visit(tree)
|
|
149
|
+
for name, lineno in visitor.found:
|
|
150
|
+
report.error(
|
|
151
|
+
"RESOURCE_MUTATES",
|
|
152
|
+
f"resource '{spec.name}' appears to mutate external state via a "
|
|
153
|
+
f"'.{name}(...)' call (line {lineno}); resources must be read-only — "
|
|
154
|
+
f"declare this as an @action instead (§5).",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _check_manifest_and_meta(connector: Connector, report: ValidationReport) -> None:
|
|
159
|
+
if not (connector.name or "").strip():
|
|
160
|
+
report.error("NAME_MISSING", "connector has no name.")
|
|
161
|
+
if not _SEMVER.match(connector.version or ""):
|
|
162
|
+
report.warn("VERSION_SEMVER", f"version '{connector.version}' is not semver x.y.z (§11).")
|
|
163
|
+
try:
|
|
164
|
+
manifest = connector.build_manifest()
|
|
165
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
166
|
+
report.error("MANIFEST_BUILD", f"build_manifest() failed: {exc!r}")
|
|
167
|
+
manifest = None
|
|
168
|
+
|
|
169
|
+
# SDK compatibility (§11): the connector must target this SDK's major version.
|
|
170
|
+
if manifest is not None:
|
|
171
|
+
from .compat import check_manifest_compatibility
|
|
172
|
+
|
|
173
|
+
compat = check_manifest_compatibility(manifest)
|
|
174
|
+
if not compat.compatible:
|
|
175
|
+
report.error("SDK_INCOMPATIBLE", compat.reason)
|
|
176
|
+
|
|
177
|
+
# Deployment honesty (§13).
|
|
178
|
+
if connector.air_gapped_capable and connector.requires_network:
|
|
179
|
+
report.error(
|
|
180
|
+
"DEPLOY_CONTRADICTION",
|
|
181
|
+
"connector declares air_gapped_capable=True but requires_network=True; "
|
|
182
|
+
"an air-gapped connector cannot require external network access (§13).",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Least-privilege scopes (§13).
|
|
186
|
+
for scope in connector.effective_auth_scopes:
|
|
187
|
+
if _BROAD_SCOPE.search(scope):
|
|
188
|
+
report.warn(
|
|
189
|
+
"SCOPE_BROAD",
|
|
190
|
+
f"OAuth scope '{scope}' looks broader than least-privilege (§13).",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def validate_connector(connector: Connector) -> ValidationReport:
|
|
195
|
+
"""Run all checks against a loaded connector and return a report."""
|
|
196
|
+
report = ValidationReport(connector_name=connector.name or "<unnamed>")
|
|
197
|
+
_check_manifest_and_meta(connector, report)
|
|
198
|
+
_check_descriptions(connector, report)
|
|
199
|
+
_check_actions(connector, report)
|
|
200
|
+
_check_resources_are_readonly(connector, report)
|
|
201
|
+
return report
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepquery-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Connector Development Kit for Deep Query — build MCP-emitting connectors with built-in read/action safety classification and provenance.
|
|
5
|
+
Project-URL: Homepage, https://github.com/GilbertAshivaka/deepquery-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/GilbertAshivaka/deepquery-sdk
|
|
7
|
+
Project-URL: Changelog, https://github.com/GilbertAshivaka/deepquery-sdk/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Documentation, https://github.com/GilbertAshivaka/deepquery-sdk/blob/main/README.md
|
|
9
|
+
Author-email: Gilbert Ashivaka <gilbertashivaka@gmail.com>
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: connector,deepquery,mcp,model-context-protocol,rag
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: mcp<2,>=1.10
|
|
24
|
+
Requires-Dist: pydantic<3,>=2
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# DeepQuerySDK — Connector Development Kit
|
|
32
|
+
|
|
33
|
+
Build connectors for Deep Query. A connector you write with this SDK **emits a
|
|
34
|
+
standard [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server**
|
|
35
|
+
— so Deep Query consumes it through the exact same interface as any public MCP
|
|
36
|
+
server. You never touch JSON-RPC, transport, or schema plumbing.
|
|
37
|
+
|
|
38
|
+
> **Status: v1.0.0 — all four build phases complete.** Core contracts (Phase 1),
|
|
39
|
+
> the gated **preview → approve → execute / reject** lifecycle + OAuth 2.1 PKCE
|
|
40
|
+
> scaffolding + credential injection (Phase 2), the `deepquery` **CLI** and
|
|
41
|
+
> **mock-agent harness** with the §5/§6/§13 contracts encoded as `validate`
|
|
42
|
+
> checks (Phase 3), and the semver **compatibility contract** + packaging
|
|
43
|
+
> (Phase 4). The SDK contract is at **major version 1**: connectors declaring
|
|
44
|
+
> `sdk_major_version = 1` are loadable by this runtime.
|
|
45
|
+
|
|
46
|
+
## What you define
|
|
47
|
+
|
|
48
|
+
A connector is a subclass of `Connector` that declares three things:
|
|
49
|
+
|
|
50
|
+
| You define | What it is | Gated? |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| **Resources** | read-only data the agent can retrieve and **cite** | no |
|
|
53
|
+
| **Actions** | operations that change external state | yes (preview → execute) |
|
|
54
|
+
| **Auth** | how the connector authenticates to the external system | n/a |
|
|
55
|
+
|
|
56
|
+
Every read carries a **provenance envelope** so live data can be cited honestly.
|
|
57
|
+
Every action is tagged `dq.mutates: true` in the emitted MCP server so Deep
|
|
58
|
+
Query's approval gate knows it must be confirmed by a human.
|
|
59
|
+
|
|
60
|
+
## Install (development)
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
# from the DeepQuerySDK/ folder, using the bundled venv
|
|
64
|
+
venv\Scripts\python.exe -m pip install -e .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 60-second example
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from deepquery_sdk import Connector, OAuth2Auth, resource, action
|
|
71
|
+
|
|
72
|
+
class JiraConnector(Connector):
|
|
73
|
+
name = "jira"
|
|
74
|
+
version = "0.1.0"
|
|
75
|
+
description = "Read and act on Jira issues."
|
|
76
|
+
|
|
77
|
+
# Declare OAuth 2.1 with least-privilege scopes. The gateway runs/stores the
|
|
78
|
+
# grant and injects the token; this connector never stores credentials.
|
|
79
|
+
auth = OAuth2Auth(
|
|
80
|
+
authorize_endpoint="https://auth.atlassian.com/authorize",
|
|
81
|
+
token_endpoint="https://auth.atlassian.com/oauth/token",
|
|
82
|
+
scopes=["read:jira-work", "write:jira-work"],
|
|
83
|
+
)
|
|
84
|
+
requires_network = True
|
|
85
|
+
|
|
86
|
+
@resource(description="Search Jira issues by text query.",
|
|
87
|
+
input_schema={"type": "object",
|
|
88
|
+
"properties": {"query": {"type": "string"}},
|
|
89
|
+
"required": ["query"]})
|
|
90
|
+
def search_issues(self, query: str):
|
|
91
|
+
# ... call the real Jira API here ...
|
|
92
|
+
return [
|
|
93
|
+
self.cite(
|
|
94
|
+
{"key": "DQ-431", "summary": "Login flow broken", "status": "In Review"},
|
|
95
|
+
source_object_id="DQ-431",
|
|
96
|
+
title_or_label="DQ-431 — Login flow broken",
|
|
97
|
+
deep_link="https://example.atlassian.net/browse/DQ-431",
|
|
98
|
+
mutability_note="live status field",
|
|
99
|
+
)
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
@action(description="Create a new Jira issue.",
|
|
103
|
+
input_schema={"type": "object",
|
|
104
|
+
"properties": {"project": {"type": "string"},
|
|
105
|
+
"summary": {"type": "string"}},
|
|
106
|
+
"required": ["project", "summary"]})
|
|
107
|
+
def create_issue(self, project: str, summary: str):
|
|
108
|
+
# only ever called after the approval gate confirms the preview.
|
|
109
|
+
auth = self.apply_auth() # headers built from the injected credential
|
|
110
|
+
return {"created": f"{project}-NEW", "summary": summary}
|
|
111
|
+
|
|
112
|
+
@create_issue.preview
|
|
113
|
+
def _(self, project: str, summary: str) -> str:
|
|
114
|
+
return f"Will create a new issue in project '{project}' titled '{summary}'."
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Emit and serve it as an MCP server (the gateway injects the credential):
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from deepquery_sdk import Credential, static_credential_provider
|
|
121
|
+
from deepquery_sdk.mcp_emit import run_stdio
|
|
122
|
+
|
|
123
|
+
connector = JiraConnector()
|
|
124
|
+
connector.set_credential_provider(static_credential_provider(Credential(token="...")))
|
|
125
|
+
run_stdio(connector)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## The gated action lifecycle
|
|
129
|
+
|
|
130
|
+
Calling an action does **not** execute it. It returns a preview and a single-use
|
|
131
|
+
approval token; the gateway drives the decision through two control tools:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
call create_issue(args) -> { status: "preview", approval_token, preview, arguments }
|
|
135
|
+
call dq.execute_action(token) -> runs execute() for exactly those args (after human approval)
|
|
136
|
+
call dq.reject_action(token) -> discards the action; it never runs
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The token binds the previewed arguments, so execute can never drift from what the
|
|
140
|
+
human approved, and a rejected/used token can never run.
|
|
141
|
+
|
|
142
|
+
## The `deepquery` CLI
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
deepquery scaffold acme-crm # generate a new connector from the template
|
|
146
|
+
deepquery validate connector.py # enforce the §5/§6/§13 safety contracts
|
|
147
|
+
deepquery manifest connector.py # print/export the connector manifest
|
|
148
|
+
deepquery run-dev connector.py # drive it interactively with the mock agent
|
|
149
|
+
deepquery emit connector.py --out dist/ # produce the deployable MCP server artifact
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A target can be a `path/to/connector.py`, a directory containing `connector.py`,
|
|
153
|
+
or a `module.path:ClassName`. `validate` is the gatekeeper: it statically scans
|
|
154
|
+
`@resource` bodies for mutating calls and **fails a resource that writes** (a
|
|
155
|
+
misclassified action), plus checks descriptions, manifest, deployment honesty,
|
|
156
|
+
and least-privilege scopes.
|
|
157
|
+
|
|
158
|
+
The dev harness (`deepquery_sdk.harness.MockAgent`) discovers a connector over a
|
|
159
|
+
real in-memory MCP session and drives reads (inspecting provenance) and the full
|
|
160
|
+
preview → approve / reject action flow — the same sequence the real Agent Layer
|
|
161
|
+
uses.
|
|
162
|
+
|
|
163
|
+
## Try it without the test suite
|
|
164
|
+
|
|
165
|
+
```powershell
|
|
166
|
+
# read path + classification + provenance, over a real stdio MCP server
|
|
167
|
+
venv\Scripts\python.exe examples\manual_client.py
|
|
168
|
+
|
|
169
|
+
# the full preview -> approve -> execute and preview -> reject flow
|
|
170
|
+
venv\Scripts\python.exe examples\manual_action_flow.py
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
See [`examples/jira_connector/`](examples/jira_connector/) for the full runnable
|
|
174
|
+
connector used in the Phase 1 and Phase 2 tests.
|
|
175
|
+
|
|
176
|
+
## Versioning & compatibility
|
|
177
|
+
|
|
178
|
+
The SDK follows [SemVer](https://semver.org); see [CHANGELOG.md](CHANGELOG.md).
|
|
179
|
+
Every connector manifest declares the SDK **major** version it targets, and the
|
|
180
|
+
gateway calls `assert_compatible(manifest)` before loading — refusing a connector
|
|
181
|
+
built against an incompatible major with a clear error. Breaking changes ship
|
|
182
|
+
with a guide in [MIGRATIONS.md](MIGRATIONS.md).
|
|
183
|
+
|
|
184
|
+
## Release / publishing (maintainers)
|
|
185
|
+
|
|
186
|
+
Distributions are built with `python -m build` into `dist/`. To publish to PyPI:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
python -m build # build sdist + wheel into dist/
|
|
190
|
+
python -m twine check dist/* # validate metadata
|
|
191
|
+
python -m twine upload dist/* # publish (requires PyPI credentials)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Tag the release `v<version>` and ensure `version` in `pyproject.toml` matches
|
|
195
|
+
`SDK_VERSION` (a test enforces this against the installed package).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
deepquery_sdk/__init__.py,sha256=ljlvmc-k3UNY5wYmCVa_s0aZbG49aXaTWnXjP2aCuCM,2323
|
|
2
|
+
deepquery_sdk/action.py,sha256=5x1h1xyefYOMbkhtyPN9Vf9mjkI7HSLSh-Ky9yFPAr4,3516
|
|
3
|
+
deepquery_sdk/classification.py,sha256=8u3rvGlmK5Qku6l5p1fqxMjKNqqdbnuawDzS98SrkDw,1536
|
|
4
|
+
deepquery_sdk/compat.py,sha256=0r_dOdT8PZYMsVXkn13wiXAdLeFYrfAk2pGGbo8jIOU,2924
|
|
5
|
+
deepquery_sdk/connector.py,sha256=XAyovORa8VATBh5L5T-VU4dQM3Kew0Oa1WsNXjBDKEs,11824
|
|
6
|
+
deepquery_sdk/gate.py,sha256=3SeGuNJLKn3VJL7lflDgnWZuC8M-MW1L4y7FT_5s9lI,3171
|
|
7
|
+
deepquery_sdk/manifest.py,sha256=sb2TO5s5QAnyqIrgUtWFXSUP4SrAiKmaWnnJbWUlLa8,2654
|
|
8
|
+
deepquery_sdk/provenance.py,sha256=TmYNrjOfxAOZI3oncGHIwZo9Af2nI2cLiaAclZvkoDY,2497
|
|
9
|
+
deepquery_sdk/resource.py,sha256=bsflvP09c__BQLdnCYORD6PMTOXAULDVI2t2m3QBv4k,2051
|
|
10
|
+
deepquery_sdk/validation.py,sha256=9EvMwkeHLY0diF7thGK9XmAelf_F04zbCBZ245I9tDw,7682
|
|
11
|
+
deepquery_sdk/auth/__init__.py,sha256=SEDgUezt3x3UiT2avO0HsF_hoiTEPDaWI1TOiolza0Y,1017
|
|
12
|
+
deepquery_sdk/auth/base.py,sha256=JxErZ2U6PQOU6DJTqNhReZMMlhvpxSRm1wtTFUtyQKA,3287
|
|
13
|
+
deepquery_sdk/auth/oauth2.py,sha256=fAbZv5Ny-9vDHv0P3_Ait94WVZ4DSCatolISjRcjbOw,3004
|
|
14
|
+
deepquery_sdk/auth/strategies.py,sha256=SgyMErzqqgLRVtZ5udt7DwJpiEJG-ECCyBF89v7qPTY,3285
|
|
15
|
+
deepquery_sdk/cli/__init__.py,sha256=iAmOGZvrhYJ20s1BkFmBB47A0sQx1fedA07sC1bibHQ,139
|
|
16
|
+
deepquery_sdk/cli/__main__.py,sha256=0LgqR-qjp_UZVMpjbMXkk4HhioAIpARaYkfMrwzhIQo,2513
|
|
17
|
+
deepquery_sdk/cli/commands.py,sha256=8-4oApFvnblBOxd2Cd8lj8OtiPA5xcS9KGjIrCIIHMo,7563
|
|
18
|
+
deepquery_sdk/cli/loader.py,sha256=WevWoXpHQjp5v92aF6N-IhtyuvIQ2bXSRRpfGjDr4Yk,3326
|
|
19
|
+
deepquery_sdk/harness/__init__.py,sha256=6n4u0RRqgpVgefEqn7XvuNKwf1F-9HCwQ1KwTCWCZR8,164
|
|
20
|
+
deepquery_sdk/harness/mock_agent.py,sha256=1teXGsc7TnakTNSrusfGfuxaZtWefSPCIHZPDCGI7Zw,5478
|
|
21
|
+
deepquery_sdk/mcp_emit/__init__.py,sha256=qMZWxucslEXOxQhfnf65LifjighelKb1MDndA7GF_Rs,205
|
|
22
|
+
deepquery_sdk/mcp_emit/emitter.py,sha256=8_vW-5atljba7X5u0lngILkoec42-GTs7ygKcEkQ89I,6806
|
|
23
|
+
deepquery_sdk/templates/connector/.gitignore.tmpl,sha256=XiYls0YMt-7cQ6bDLIs2bjzgovbsp2DA8PN6OL_0Mbc,81
|
|
24
|
+
deepquery_sdk/templates/connector/README.md.tmpl,sha256=NrTNm-8ww4YAeMBb-VkbyTG33JwMYgE6QI4qufURyqI,883
|
|
25
|
+
deepquery_sdk/templates/connector/connector.py.tmpl,sha256=MwQKzlBg-fiAwrb1MtaxG0eeqj6Zu-cPAQpHL3sXnHc,2907
|
|
26
|
+
deepquery_sdk/templates/connector/pyproject.toml.tmpl,sha256=k4wGRd_UNMmercQ6U5RSaQpUgu43wNpKX9YSMM82l9g,569
|
|
27
|
+
deepquery_sdk/templates/connector/tests/test_connector.py.tmpl,sha256=Vvh0hb0xn6k6UR7woHStJLN78Hty5c2oScmUUMNTDtU,1510
|
|
28
|
+
deepquery_sdk-1.0.0.dist-info/METADATA,sha256=gqYaknbqGcqA_uF_I1QOia-NN9UNyn_wclAymlscMi4,8478
|
|
29
|
+
deepquery_sdk-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
30
|
+
deepquery_sdk-1.0.0.dist-info/entry_points.txt,sha256=8PgvowkTPbukZyAxzSKuRrBRVG4uO3_ubHQvgJD3Z1Q,62
|
|
31
|
+
deepquery_sdk-1.0.0.dist-info/licenses/LICENSE,sha256=f1SM0QFeSzU-QMK2DLqc0ZXyonbBk_Mp01DU3QDvZlM,10575
|
|
32
|
+
deepquery_sdk-1.0.0.dist-info/RECORD,,
|