adp-hypervisor 0.1.0.dev0__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.
- adp_hypervisor/__init__.py +28 -0
- adp_hypervisor/__main__.py +289 -0
- adp_hypervisor/handlers/__init__.py +35 -0
- adp_hypervisor/handlers/base.py +45 -0
- adp_hypervisor/handlers/describe.py +286 -0
- adp_hypervisor/handlers/discover.py +228 -0
- adp_hypervisor/handlers/execute.py +211 -0
- adp_hypervisor/handlers/initialize.py +123 -0
- adp_hypervisor/handlers/ping.py +53 -0
- adp_hypervisor/handlers/validate.py +387 -0
- adp_hypervisor/manifest/__init__.py +84 -0
- adp_hypervisor/manifest/index.py +446 -0
- adp_hypervisor/manifest/physical.py +177 -0
- adp_hypervisor/manifest/policy.py +210 -0
- adp_hypervisor/manifest/provider.py +56 -0
- adp_hypervisor/manifest/semantic.py +92 -0
- adp_hypervisor/manifest/yaml_provider.py +108 -0
- adp_hypervisor/policy/__init__.py +33 -0
- adp_hypervisor/policy/authenticator.py +48 -0
- adp_hypervisor/policy/basic_authenticator.py +114 -0
- adp_hypervisor/policy/enforcer.py +232 -0
- adp_hypervisor/policy/role_resolver.py +61 -0
- adp_hypervisor/policy/yaml_role_resolver.py +64 -0
- adp_hypervisor/protocol/__init__.py +221 -0
- adp_hypervisor/protocol/dispatcher.py +487 -0
- adp_hypervisor/protocol/errors.py +206 -0
- adp_hypervisor/protocol/jsonrpc.py +105 -0
- adp_hypervisor/protocol/types.py +840 -0
- adp_hypervisor/server.py +278 -0
- adp_hypervisor/transport/__init__.py +23 -0
- adp_hypervisor/transport/base.py +54 -0
- adp_hypervisor/transport/stdio.py +152 -0
- adp_hypervisor-0.1.0.dev0.dist-info/METADATA +236 -0
- adp_hypervisor-0.1.0.dev0.dist-info/RECORD +53 -0
- adp_hypervisor-0.1.0.dev0.dist-info/WHEEL +4 -0
- adp_hypervisor-0.1.0.dev0.dist-info/licenses/LICENSE +217 -0
- adp_hypervisor-0.1.0.dev0.dist-info/licenses/NOTICE +2 -0
- backends/__init__.py +43 -0
- backends/base.py +91 -0
- backends/blob_storage/__init__.py +74 -0
- backends/blob_storage/backend.py +35 -0
- backends/blob_storage/local.py +777 -0
- backends/credentials.py +82 -0
- backends/nosql/__init__.py +25 -0
- backends/nosql/backend.py +35 -0
- backends/nosql/mongodb.py +519 -0
- backends/rdbms/__init__.py +25 -0
- backends/rdbms/backend.py +268 -0
- backends/rdbms/postgres.py +133 -0
- backends/registry.py +111 -0
- backends/vector/__init__.py +25 -0
- backends/vector/backend.py +112 -0
- backends/vector/pgvector.py +284 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright 2026 Datastrato, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""ADP Hypervisor - Agentic Data Protocol Python Implementation."""
|
|
16
|
+
|
|
17
|
+
__all__ = ["ADPServer"]
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def __getattr__(name: str) -> object:
|
|
23
|
+
"""Lazy import to avoid circular dependency with backends."""
|
|
24
|
+
if name == "ADPServer":
|
|
25
|
+
from adp_hypervisor.server import ADPServer
|
|
26
|
+
|
|
27
|
+
return ADPServer
|
|
28
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Copyright 2026 Datastrato, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
ADP Hypervisor CLI entry point.
|
|
17
|
+
|
|
18
|
+
Starts the ADP server with the given configuration directory,
|
|
19
|
+
log level, and transport type.
|
|
20
|
+
|
|
21
|
+
Usage::
|
|
22
|
+
|
|
23
|
+
python -m adp_hypervisor --config /path/to/manifests
|
|
24
|
+
python -m adp_hypervisor --config /path/to/manifests --log-level DEBUG
|
|
25
|
+
python -m adp_hypervisor --config /path/to/manifests --transport stdio
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import asyncio
|
|
30
|
+
import logging
|
|
31
|
+
import logging.config
|
|
32
|
+
import logging.handlers
|
|
33
|
+
import sys
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
import yaml
|
|
38
|
+
|
|
39
|
+
from adp_hypervisor.manifest.yaml_provider import YamlManifestProvider
|
|
40
|
+
from adp_hypervisor.policy import UserRoleConfig, YamlRoleResolver
|
|
41
|
+
from adp_hypervisor.server import ADPServer
|
|
42
|
+
from adp_hypervisor.transport.stdio import StdioTransport
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
_VALID_LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
|
|
47
|
+
_VALID_TRANSPORTS = ("stdio", "http")
|
|
48
|
+
|
|
49
|
+
# Built-in default logging config, matches conf/logging_conf.yaml.template.
|
|
50
|
+
# Used when no logging_conf.yaml is present in the config directory.
|
|
51
|
+
_DEFAULT_LOGGING_CONFIG: dict[str, Any] = {
|
|
52
|
+
"version": 1,
|
|
53
|
+
"disable_existing_loggers": False,
|
|
54
|
+
"formatters": {
|
|
55
|
+
"standard": {
|
|
56
|
+
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
57
|
+
"datefmt": "%Y-%m-%dT%H:%M:%S",
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"handlers": {
|
|
61
|
+
"console": {
|
|
62
|
+
"class": "logging.StreamHandler",
|
|
63
|
+
"formatter": "standard",
|
|
64
|
+
"stream": "ext://sys.stderr",
|
|
65
|
+
},
|
|
66
|
+
"file_handler": {
|
|
67
|
+
"class": "logging.handlers.RotatingFileHandler",
|
|
68
|
+
"formatter": "standard",
|
|
69
|
+
"filename": "./hypervisor-logs/hypervisor.log",
|
|
70
|
+
"maxBytes": 10485760,
|
|
71
|
+
"backupCount": 5,
|
|
72
|
+
"encoding": "utf-8",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"root": {
|
|
76
|
+
"level": "INFO",
|
|
77
|
+
"handlers": ["console", "file_handler"],
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
83
|
+
"""Build the CLI argument parser."""
|
|
84
|
+
parser = argparse.ArgumentParser(
|
|
85
|
+
prog="adp_hypervisor",
|
|
86
|
+
description="ADP Hypervisor - Agentic Data Protocol server",
|
|
87
|
+
)
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--config",
|
|
90
|
+
required=True,
|
|
91
|
+
help="Path to the manifest directory containing physical.yaml, "
|
|
92
|
+
"semantic.yaml, and policy.yaml",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--log-level",
|
|
96
|
+
default=None,
|
|
97
|
+
choices=_VALID_LOG_LEVELS,
|
|
98
|
+
help="Override the root logging level (default: level from logging_conf.yaml or INFO)",
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--transport",
|
|
102
|
+
default="stdio",
|
|
103
|
+
choices=_VALID_TRANSPORTS,
|
|
104
|
+
help="Transport type (default: stdio)",
|
|
105
|
+
)
|
|
106
|
+
return parser
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _load_logging_config(config_dir: Path) -> dict[str, Any]:
|
|
110
|
+
"""Load logging configuration from config_dir/logging_conf.yaml.
|
|
111
|
+
|
|
112
|
+
Falls back to the built-in default when the file is absent.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
config_dir: The runtime config directory.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A dict suitable for logging.config.dictConfig.
|
|
119
|
+
"""
|
|
120
|
+
logging_conf_path = config_dir / "logging_conf.yaml"
|
|
121
|
+
if logging_conf_path.exists():
|
|
122
|
+
try:
|
|
123
|
+
raw = yaml.safe_load(logging_conf_path.read_text(encoding="utf-8"))
|
|
124
|
+
if isinstance(raw, dict):
|
|
125
|
+
return raw
|
|
126
|
+
print(
|
|
127
|
+
f"Warning: {logging_conf_path} is not a YAML mapping (got {type(raw).__name__}), "
|
|
128
|
+
"using built-in logging defaults.",
|
|
129
|
+
file=sys.stderr,
|
|
130
|
+
)
|
|
131
|
+
except (yaml.YAMLError, OSError):
|
|
132
|
+
print(
|
|
133
|
+
f"Warning: failed to parse {logging_conf_path}, using built-in logging defaults.",
|
|
134
|
+
file=sys.stderr,
|
|
135
|
+
)
|
|
136
|
+
return _DEFAULT_LOGGING_CONFIG
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _ensure_log_directories(config: dict[str, Any]) -> None:
|
|
140
|
+
"""Create parent directories for all file-based log handlers.
|
|
141
|
+
|
|
142
|
+
Scans every handler in the config dict, resolves the parent directory
|
|
143
|
+
for any handler that has a ``filename`` key, and creates it if missing.
|
|
144
|
+
Emits a startup notice to stderr for each resolved log file path.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
config: The logging config dict (as passed to dictConfig).
|
|
148
|
+
"""
|
|
149
|
+
handlers = config.get("handlers", {})
|
|
150
|
+
for _name, handler_cfg in handlers.items():
|
|
151
|
+
filename = handler_cfg.get("filename")
|
|
152
|
+
if not filename:
|
|
153
|
+
continue
|
|
154
|
+
log_path = Path(filename).resolve()
|
|
155
|
+
log_dir = log_path.parent
|
|
156
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
print(
|
|
158
|
+
f"ADP Hypervisor: logging to file {log_path}",
|
|
159
|
+
file=sys.stderr,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _configure_logging(config_dir: Path, level_override: str | None) -> None:
|
|
164
|
+
"""Load and apply logging configuration.
|
|
165
|
+
|
|
166
|
+
Reads logging_conf.yaml from config_dir (falls back to built-in defaults),
|
|
167
|
+
ensures all file-handler directories exist, then applies the config via
|
|
168
|
+
dictConfig. Overrides the root log level when --log-level is provided.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
config_dir: The runtime config directory.
|
|
172
|
+
level_override: Optional log level string (e.g. "DEBUG") from --log-level.
|
|
173
|
+
"""
|
|
174
|
+
config = _load_logging_config(config_dir)
|
|
175
|
+
|
|
176
|
+
if level_override is not None:
|
|
177
|
+
config.setdefault("root", {})["level"] = level_override
|
|
178
|
+
|
|
179
|
+
_ensure_log_directories(config)
|
|
180
|
+
logging.config.dictConfig(config)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _create_yaml_provider(config_dir: Path) -> YamlManifestProvider:
|
|
184
|
+
"""Create a YAML manifest provider from a config directory.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
config_dir: Path to the directory containing physical.yaml,
|
|
188
|
+
semantic.yaml, and policy.yaml manifest files.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
A configured YamlManifestProvider instance.
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
FileNotFoundError: If required manifest files are missing.
|
|
195
|
+
"""
|
|
196
|
+
physical_path = config_dir / "physical.yaml"
|
|
197
|
+
semantic_path = config_dir / "semantic.yaml"
|
|
198
|
+
policy_path = config_dir / "policy.yaml"
|
|
199
|
+
|
|
200
|
+
for path in (physical_path, semantic_path):
|
|
201
|
+
if not path.exists():
|
|
202
|
+
raise FileNotFoundError(f"Manifest file not found: {path}")
|
|
203
|
+
|
|
204
|
+
# Accept a missing policy file gracefully — ACCESS enforcement uses
|
|
205
|
+
# closed-by-default semantics, so an empty policy file simply denies all.
|
|
206
|
+
if not policy_path.exists():
|
|
207
|
+
try:
|
|
208
|
+
policy_path.write_text("version: '1.0.0'\n", encoding="utf-8")
|
|
209
|
+
except OSError as exc:
|
|
210
|
+
raise FileNotFoundError(
|
|
211
|
+
f"Policy manifest not found at {policy_path} and could not be created. "
|
|
212
|
+
"Ensure the config directory is writable or provide a policy.yaml file."
|
|
213
|
+
) from exc
|
|
214
|
+
|
|
215
|
+
return YamlManifestProvider(
|
|
216
|
+
physical_path=physical_path,
|
|
217
|
+
semantic_path=semantic_path,
|
|
218
|
+
policy_path=policy_path,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _create_role_resolver(config_dir: Path) -> YamlRoleResolver:
|
|
223
|
+
"""Create a role resolver from a config directory.
|
|
224
|
+
|
|
225
|
+
Loads the user-to-role mapping from ``users.yaml`` in the config
|
|
226
|
+
directory. If the file does not exist, returns a resolver with
|
|
227
|
+
default configuration.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
config_dir: Path to the directory containing users.yaml.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
A configured YamlRoleResolver instance.
|
|
234
|
+
"""
|
|
235
|
+
users_path = config_dir / "users.yaml"
|
|
236
|
+
if not users_path.exists():
|
|
237
|
+
logger.info("No users.yaml found in %s, using default role config", config_dir)
|
|
238
|
+
return YamlRoleResolver(UserRoleConfig())
|
|
239
|
+
|
|
240
|
+
import yaml
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
raw = yaml.safe_load(users_path.read_text(encoding="utf-8"))
|
|
244
|
+
config = UserRoleConfig.model_validate(raw or {})
|
|
245
|
+
except (yaml.YAMLError, ValueError) as exc:
|
|
246
|
+
logger.error("Failed to load %s, using default role config: %s", users_path, exc)
|
|
247
|
+
return YamlRoleResolver(UserRoleConfig())
|
|
248
|
+
|
|
249
|
+
logger.info(
|
|
250
|
+
"Loaded user-role config: %d users, default_role=%r",
|
|
251
|
+
len(config.users),
|
|
252
|
+
config.default_role,
|
|
253
|
+
)
|
|
254
|
+
return YamlRoleResolver(config)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def main(args: list[str] | None = None) -> None:
|
|
258
|
+
"""Parse arguments and run the ADP server.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
args: Command-line arguments. Defaults to sys.argv[1:].
|
|
262
|
+
"""
|
|
263
|
+
parser = _build_parser()
|
|
264
|
+
parsed = parser.parse_args(args)
|
|
265
|
+
|
|
266
|
+
config_dir = Path(parsed.config)
|
|
267
|
+
_configure_logging(config_dir, parsed.log_level)
|
|
268
|
+
|
|
269
|
+
if parsed.transport == "http":
|
|
270
|
+
logger.error("HTTP transport is not yet implemented")
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
|
|
273
|
+
transport = StdioTransport()
|
|
274
|
+
provider = _create_yaml_provider(config_dir)
|
|
275
|
+
role_resolver = _create_role_resolver(config_dir)
|
|
276
|
+
server = ADPServer(manifest_provider=provider, transport=transport, role_resolver=role_resolver)
|
|
277
|
+
|
|
278
|
+
logger.info(
|
|
279
|
+
"Starting ADP Hypervisor: config=%s, transport=%s, log_level=%s",
|
|
280
|
+
parsed.config,
|
|
281
|
+
parsed.transport,
|
|
282
|
+
parsed.log_level or "from config",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
asyncio.run(server.run())
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
main()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright 2026 Datastrato, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""ADP Request Handlers."""
|
|
16
|
+
|
|
17
|
+
from adp_hypervisor.handlers.base import Handler
|
|
18
|
+
from adp_hypervisor.handlers.describe import DescribeHandler
|
|
19
|
+
from adp_hypervisor.handlers.discover import DiscoverHandler
|
|
20
|
+
from adp_hypervisor.handlers.execute import ExecuteHandler
|
|
21
|
+
from adp_hypervisor.handlers.initialize import InitializeHandler
|
|
22
|
+
from adp_hypervisor.handlers.ping import PingHandler
|
|
23
|
+
from adp_hypervisor.handlers.validate import ValidateHandler
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Base
|
|
27
|
+
"Handler",
|
|
28
|
+
# Handlers
|
|
29
|
+
"DescribeHandler",
|
|
30
|
+
"DiscoverHandler",
|
|
31
|
+
"ExecuteHandler",
|
|
32
|
+
"InitializeHandler",
|
|
33
|
+
"PingHandler",
|
|
34
|
+
"ValidateHandler",
|
|
35
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Copyright 2026 Datastrato, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Handler abstract base class.
|
|
17
|
+
|
|
18
|
+
Defines the abstract interface that all ADP request handlers must implement.
|
|
19
|
+
Each handler processes a specific JSON-RPC method (e.g., adp.initialize, adp.ping).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from abc import ABC, abstractmethod
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from pydantic import BaseModel
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Handler(ABC):
|
|
29
|
+
"""Handler abstract base class for processing ADP requests."""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def method(self) -> str:
|
|
34
|
+
"""Return the JSON-RPC method name this handler processes."""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def handle(self, params: dict[str, Any]) -> BaseModel:
|
|
38
|
+
"""Process a request and return the result.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
params: The JSON-RPC request parameters.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A Pydantic model representing the response.
|
|
45
|
+
"""
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Copyright 2026 Datastrato, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Describe Handler.
|
|
17
|
+
|
|
18
|
+
Implements the adp.describe method which returns the UsageContract
|
|
19
|
+
(field definitions and capabilities) for a specific resource and intent class.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import TYPE_CHECKING, Any
|
|
26
|
+
|
|
27
|
+
from pydantic import BaseModel
|
|
28
|
+
|
|
29
|
+
from adp_hypervisor.handlers.base import Handler
|
|
30
|
+
from adp_hypervisor.policy.enforcer import PolicyEnforcer
|
|
31
|
+
from adp_hypervisor.protocol.errors import InvalidParamsError, ResourceNotFoundError
|
|
32
|
+
from adp_hypervisor.protocol.types import (
|
|
33
|
+
Capabilities,
|
|
34
|
+
DescribeRequestParams,
|
|
35
|
+
DescribeResult,
|
|
36
|
+
Field,
|
|
37
|
+
FieldType,
|
|
38
|
+
IntentClass,
|
|
39
|
+
MutableCapability,
|
|
40
|
+
PredicateCapability,
|
|
41
|
+
PredicateOperator,
|
|
42
|
+
PredicateUsage,
|
|
43
|
+
ProjectionCapability,
|
|
44
|
+
UsageContract,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from adp_hypervisor.manifest.index import ManifestIndex
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
# Operator mapping by FieldType
|
|
53
|
+
# Operator Sets (Reusable groups of operators)
|
|
54
|
+
_BASIC_OPS = [
|
|
55
|
+
PredicateOperator.EQ,
|
|
56
|
+
PredicateOperator.NEQ,
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
_ORDERING_OPS = [
|
|
60
|
+
PredicateOperator.GT,
|
|
61
|
+
PredicateOperator.LT,
|
|
62
|
+
PredicateOperator.GTE,
|
|
63
|
+
PredicateOperator.LTE,
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
_MATCHING_OPS = [
|
|
67
|
+
PredicateOperator.LIKE,
|
|
68
|
+
PredicateOperator.ILIKE,
|
|
69
|
+
PredicateOperator.CONTAINS,
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
_SET_OPS = [
|
|
73
|
+
PredicateOperator.IN,
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# Combined Operator Groups
|
|
77
|
+
_NUMERIC_OPS = _BASIC_OPS + _ORDERING_OPS + _SET_OPS
|
|
78
|
+
_TEXT_OPS = _NUMERIC_OPS + _MATCHING_OPS
|
|
79
|
+
_CONTAINER_OPS = _BASIC_OPS + [PredicateOperator.CONTAINS]
|
|
80
|
+
_VECTOR_OPS = [PredicateOperator.SIMILAR]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Field Type Categories
|
|
84
|
+
_TEXT_TYPES = {
|
|
85
|
+
FieldType.STRING,
|
|
86
|
+
FieldType.DATE,
|
|
87
|
+
FieldType.TIMESTAMP,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_NUMERIC_TYPES = {
|
|
91
|
+
FieldType.INTEGER,
|
|
92
|
+
FieldType.FLOAT,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_BASIC_TYPES = {
|
|
96
|
+
FieldType.BOOLEAN,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_CONTAINER_TYPES = {
|
|
100
|
+
FieldType.JSON,
|
|
101
|
+
FieldType.BLOB,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_VECTOR_TYPES = {
|
|
105
|
+
FieldType.VECTOR,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Dynamic Operator Mapping Construction
|
|
110
|
+
_OPERATORS_BY_FIELD_TYPE: dict[FieldType, list[PredicateOperator]] = {}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _register_operators(field_types: set[FieldType], operators: list[PredicateOperator]) -> None:
|
|
114
|
+
for field_type in field_types:
|
|
115
|
+
_OPERATORS_BY_FIELD_TYPE[field_type] = operators
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
_register_operators(_TEXT_TYPES, _TEXT_OPS)
|
|
119
|
+
_register_operators(_NUMERIC_TYPES, _NUMERIC_OPS)
|
|
120
|
+
_register_operators(_BASIC_TYPES, _BASIC_OPS)
|
|
121
|
+
_register_operators(_CONTAINER_TYPES, _CONTAINER_OPS)
|
|
122
|
+
_register_operators(_VECTOR_TYPES, _VECTOR_OPS)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _get_operators_for_field(field_type: FieldType | None) -> list[PredicateOperator]:
|
|
126
|
+
"""Return allowed predicate operators for a given field type."""
|
|
127
|
+
if field_type is None:
|
|
128
|
+
return [PredicateOperator.EQ]
|
|
129
|
+
return list(_OPERATORS_BY_FIELD_TYPE.get(field_type, [PredicateOperator.EQ]))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _get_mandatory_field_ids(manifest_index: ManifestIndex, resource_id: str) -> set[str]:
|
|
133
|
+
"""Get field IDs that have MANDATORY_FILTER policy rules."""
|
|
134
|
+
|
|
135
|
+
policies = manifest_index.get_mandatory_filter_policies(resource_id)
|
|
136
|
+
return {p.field_id for p in policies}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _build_read_capabilities(fields: list[Field], mandatory_field_ids: set[str]) -> Capabilities:
|
|
140
|
+
"""Build capabilities for READ intent classes (LOOKUP, QUERY)."""
|
|
141
|
+
predicates = [
|
|
142
|
+
PredicateCapability(
|
|
143
|
+
field_id=f.field_id,
|
|
144
|
+
usage=(
|
|
145
|
+
PredicateUsage.REQUIRED
|
|
146
|
+
if f.field_id in mandatory_field_ids
|
|
147
|
+
else PredicateUsage.OPTIONAL
|
|
148
|
+
),
|
|
149
|
+
operators=_get_operators_for_field(f.type),
|
|
150
|
+
)
|
|
151
|
+
for f in fields
|
|
152
|
+
]
|
|
153
|
+
projections = [ProjectionCapability(field_id=f.field_id) for f in fields]
|
|
154
|
+
return Capabilities(predicates=predicates, projections=projections)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _build_write_capabilities(fields: list[Field]) -> Capabilities:
|
|
158
|
+
"""Build capabilities for WRITE intent classes (INGEST, REVISE)."""
|
|
159
|
+
mutables = [MutableCapability(field_id=f.field_id) for f in fields]
|
|
160
|
+
return Capabilities(mutables=mutables)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _is_read_intent(intent_class: IntentClass) -> bool:
|
|
164
|
+
return intent_class in (IntentClass.LOOKUP, IntentClass.QUERY)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class DescribeHandler(Handler):
|
|
168
|
+
"""Handler for the adp.describe method.
|
|
169
|
+
|
|
170
|
+
Returns the UsageContract (field definitions and capabilities) for a
|
|
171
|
+
specific resource and intent class. Capabilities are auto-derived from
|
|
172
|
+
the resource's field types and policy rules.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, manifest_index: ManifestIndex, policy_enforcer: PolicyEnforcer) -> None:
|
|
176
|
+
"""Initialize the handler.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
manifest_index: The manifest index to look up resources and policies.
|
|
180
|
+
policy_enforcer: The policy enforcer to check access.
|
|
181
|
+
"""
|
|
182
|
+
self._manifest_index = manifest_index
|
|
183
|
+
self._policy_enforcer = policy_enforcer
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def method(self) -> str:
|
|
187
|
+
"""Return the JSON-RPC method name this handler processes."""
|
|
188
|
+
return "adp.describe"
|
|
189
|
+
|
|
190
|
+
async def handle(self, params: dict[str, Any]) -> BaseModel:
|
|
191
|
+
"""Process a describe request.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
params: The JSON-RPC request parameters.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
A DescribeResult with the usage contract for the requested resource.
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
InvalidParamsError: If the intent class is invalid or unsupported.
|
|
201
|
+
ResourceNotFoundError: If the resource does not exist.
|
|
202
|
+
"""
|
|
203
|
+
request = DescribeRequestParams.model_validate(params)
|
|
204
|
+
|
|
205
|
+
role = self._policy_enforcer.resolve_role(params)
|
|
206
|
+
|
|
207
|
+
self._validate_intent_class(request.intent_class)
|
|
208
|
+
|
|
209
|
+
resource = self._manifest_index.get_resource(request.resource_id, version=request.version)
|
|
210
|
+
if resource is None:
|
|
211
|
+
raise ResourceNotFoundError(
|
|
212
|
+
f"Resource not found: {request.resource_id!r}"
|
|
213
|
+
+ (f" version={request.version}" if request.version is not None else "")
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
self._validate_resource_supports_intent(
|
|
217
|
+
request.resource_id, request.intent_class, resource.intent_classes
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
self._policy_enforcer.check_access(request.resource_id, role, request.intent_class)
|
|
221
|
+
|
|
222
|
+
fields = self._extract_fields(resource)
|
|
223
|
+
capabilities = self._build_capabilities(fields, request.intent_class, request.resource_id)
|
|
224
|
+
|
|
225
|
+
logger.info(
|
|
226
|
+
"Describe: resource=%s, version=%s, intent_class=%s, fields=%d",
|
|
227
|
+
request.resource_id,
|
|
228
|
+
resource.version,
|
|
229
|
+
request.intent_class,
|
|
230
|
+
len(fields),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return DescribeResult(
|
|
234
|
+
resource_id=request.resource_id,
|
|
235
|
+
version=resource.version or 1,
|
|
236
|
+
intent_class=request.intent_class,
|
|
237
|
+
usage_contract=UsageContract(fields=fields, capabilities=capabilities),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _validate_intent_class(self, intent_class: IntentClass) -> None:
|
|
241
|
+
if intent_class == IntentClass.WILDCARD:
|
|
242
|
+
raise InvalidParamsError(
|
|
243
|
+
"WILDCARD intent class is not allowed in describe requests. "
|
|
244
|
+
"Please specify a concrete intent class (LOOKUP, QUERY, INGEST, REVISE)."
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def _validate_resource_supports_intent(
|
|
248
|
+
self,
|
|
249
|
+
resource_id: str,
|
|
250
|
+
intent_class: IntentClass,
|
|
251
|
+
resource_intent_classes: list[IntentClass],
|
|
252
|
+
) -> None:
|
|
253
|
+
if not resource_intent_classes:
|
|
254
|
+
raise InvalidParamsError(
|
|
255
|
+
f"Resource {resource_id!r} does not declare any supported intent classes. "
|
|
256
|
+
"Use adp.discover to find resources supporting your intent class."
|
|
257
|
+
)
|
|
258
|
+
if (
|
|
259
|
+
intent_class not in resource_intent_classes
|
|
260
|
+
and IntentClass.WILDCARD not in resource_intent_classes
|
|
261
|
+
):
|
|
262
|
+
supported = ", ".join(ic.value for ic in resource_intent_classes)
|
|
263
|
+
raise InvalidParamsError(
|
|
264
|
+
f"Resource {resource_id!r} does not support intent class {intent_class.value}. "
|
|
265
|
+
f"Supported intent classes: {supported}. "
|
|
266
|
+
"Use adp.discover to find resources supporting your intent class."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _extract_fields(self, resource: Any) -> list[Field]:
|
|
270
|
+
"""Extract fields from the resource's source definition."""
|
|
271
|
+
if resource.source_definition.fields:
|
|
272
|
+
return list(resource.source_definition.fields)
|
|
273
|
+
return []
|
|
274
|
+
|
|
275
|
+
def _build_capabilities(
|
|
276
|
+
self,
|
|
277
|
+
fields: list[Field],
|
|
278
|
+
intent_class: IntentClass,
|
|
279
|
+
resource_id: str,
|
|
280
|
+
) -> Capabilities:
|
|
281
|
+
if _is_read_intent(intent_class):
|
|
282
|
+
# TODO: enforce policy rules once the policy spec is finalized.
|
|
283
|
+
# Use _get_mandatory_field_ids(self._manifest_index, resource_id) to
|
|
284
|
+
# derive REQUIRED predicates from MandatoryFilterRule entries.
|
|
285
|
+
return _build_read_capabilities(fields, mandatory_field_ids=set())
|
|
286
|
+
return _build_write_capabilities(fields)
|