mrpd 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mrpd/__init__.py +1 -0
- mrpd/adapters/__init__.py +1 -0
- mrpd/api/app.py +8 -0
- mrpd/api/routes.py +200 -0
- mrpd/cli.py +182 -0
- mrpd/commands/__init__.py +1 -0
- mrpd/commands/bridge_mcp.py +189 -0
- mrpd/commands/bridge_openapi.py +279 -0
- mrpd/commands/init_provider.py +176 -0
- mrpd/commands/mrpify_mcp.py +80 -0
- mrpd/commands/mrpify_openapi.py +80 -0
- mrpd/commands/publish.py +102 -0
- mrpd/commands/route.py +126 -0
- mrpd/commands/run.py +169 -0
- mrpd/commands/serve.py +13 -0
- mrpd/commands/validate.py +71 -0
- mrpd/core/__init__.py +1 -0
- mrpd/core/artifacts.py +39 -0
- mrpd/core/config.py +26 -0
- mrpd/core/defaults.py +7 -0
- mrpd/core/envelopes.py +29 -0
- mrpd/core/errors.py +37 -0
- mrpd/core/evidence.py +17 -0
- mrpd/core/models.py +37 -0
- mrpd/core/provider.py +100 -0
- mrpd/core/registry.py +115 -0
- mrpd/core/schema.py +64 -0
- mrpd/core/scoring.py +92 -0
- mrpd/core/util.py +27 -0
- mrpd/spec/__init__.py +1 -0
- mrpd/spec/fixtures/README.md +8 -0
- mrpd/spec/fixtures/invalid/bad_msg_type.json +8 -0
- mrpd/spec/fixtures/invalid/missing_required_fields.json +7 -0
- mrpd/spec/fixtures/valid/discover.json +13 -0
- mrpd/spec/fixtures/valid/error.json +13 -0
- mrpd/spec/fixtures/valid/offer.json +20 -0
- mrpd/spec/schemas/envelope.schema.json +102 -0
- mrpd/spec/schemas/manifest.schema.json +52 -0
- mrpd/spec/schemas/payloads/discover.schema.json +23 -0
- mrpd/spec/schemas/payloads/error.schema.json +15 -0
- mrpd/spec/schemas/payloads/evidence.schema.json +17 -0
- mrpd/spec/schemas/payloads/execute.schema.json +14 -0
- mrpd/spec/schemas/payloads/negotiate.schema.json +15 -0
- mrpd/spec/schemas/payloads/offer.schema.json +30 -0
- mrpd/spec/schemas/types/artifact.schema.json +15 -0
- mrpd/spec/schemas/types/input.schema.json +20 -0
- mrpd-0.1.0.dist-info/METADATA +104 -0
- mrpd-0.1.0.dist-info/RECORD +51 -0
- mrpd-0.1.0.dist-info/WHEEL +5 -0
- mrpd-0.1.0.dist-info/entry_points.txt +2 -0
- mrpd-0.1.0.dist-info/top_level.txt +1 -0
mrpd/core/provider.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from mrpd.core.artifacts import store_bytes
|
|
8
|
+
from mrpd.core.util import approx_tokens, strip_html, utc_now_rfc3339
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SERVICE_ID = "service:mrpd"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def provider_manifest(base_url: str) -> dict[str, Any]:
|
|
15
|
+
# base_url should be full origin, e.g. http://127.0.0.1:8787
|
|
16
|
+
return {
|
|
17
|
+
"capability_id": "capability:mrp/summarize_url",
|
|
18
|
+
"capability": "summarize_url",
|
|
19
|
+
"version": "0.1",
|
|
20
|
+
"tags": ["mrp", "summarize", "web"],
|
|
21
|
+
"inputs": [{"type": "url"}],
|
|
22
|
+
"outputs": [{"type": "markdown"}, {"type": "artifact"}],
|
|
23
|
+
"constraints": {"policy": ["no_pii"]},
|
|
24
|
+
"cost": {"unit": "usd", "estimate": 0.0},
|
|
25
|
+
"latency": {"p50": "200ms"},
|
|
26
|
+
"proofs_required": [],
|
|
27
|
+
"endpoints": {
|
|
28
|
+
"discover": f"{base_url}/mrp/discover",
|
|
29
|
+
"negotiate": f"{base_url}/mrp/negotiate",
|
|
30
|
+
"execute": f"{base_url}/mrp/execute",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def offers_for_discover(_discover_payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
"route_id": "route:mrpd/summarize_url@0.1",
|
|
39
|
+
"capability": "summarize_url",
|
|
40
|
+
"confidence": 0.9,
|
|
41
|
+
"cost": {"unit": "usd", "estimate": 0.0},
|
|
42
|
+
"latency": {"p50": "200ms"},
|
|
43
|
+
"proofs": [],
|
|
44
|
+
"policy": ["no_pii"],
|
|
45
|
+
"risk": {"data_retention_days": 0, "training_use": "none", "subprocessors": []},
|
|
46
|
+
"endpoint": "/mrp/execute",
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def execute_summarize_url(inputs: list[dict[str, Any]]) -> dict[str, Any]:
|
|
52
|
+
url = None
|
|
53
|
+
for item in inputs:
|
|
54
|
+
if item.get("type") == "url":
|
|
55
|
+
url = item.get("value")
|
|
56
|
+
break
|
|
57
|
+
if not url:
|
|
58
|
+
raise ValueError("missing url input")
|
|
59
|
+
|
|
60
|
+
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
|
61
|
+
r = await client.get(url, headers={"User-Agent": "mrpd/0.1"})
|
|
62
|
+
r.raise_for_status()
|
|
63
|
+
ct = r.headers.get("content-type", "")
|
|
64
|
+
raw_bytes = r.content
|
|
65
|
+
|
|
66
|
+
text = ""
|
|
67
|
+
if "text/html" in ct:
|
|
68
|
+
text = strip_html(raw_bytes.decode("utf-8", "replace"))
|
|
69
|
+
else:
|
|
70
|
+
# best effort
|
|
71
|
+
text = raw_bytes.decode("utf-8", "replace")
|
|
72
|
+
|
|
73
|
+
# store artifact of extracted text
|
|
74
|
+
artifact = store_bytes(text.encode("utf-8"), mime="text/plain", suffix=".txt")
|
|
75
|
+
|
|
76
|
+
# crude summary: first ~1200 chars
|
|
77
|
+
summary = text[:1200]
|
|
78
|
+
if len(text) > 1200:
|
|
79
|
+
summary += "\n\n…"
|
|
80
|
+
|
|
81
|
+
tokens_in = approx_tokens(text)
|
|
82
|
+
tokens_out = approx_tokens(summary)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"outputs": [
|
|
86
|
+
{"type": "markdown", "value": f"## Summary\n\n{summary}\n"},
|
|
87
|
+
artifact,
|
|
88
|
+
],
|
|
89
|
+
"provenance": {
|
|
90
|
+
"citations": [url],
|
|
91
|
+
"timestamp": utc_now_rfc3339(),
|
|
92
|
+
"source_hashes": [artifact["hash"]],
|
|
93
|
+
},
|
|
94
|
+
"usage": {
|
|
95
|
+
"tokens_in_est": tokens_in,
|
|
96
|
+
"tokens_out_est": tokens_out,
|
|
97
|
+
"bytes_in": len(raw_bytes),
|
|
98
|
+
"bytes_text": len(text.encode("utf-8")),
|
|
99
|
+
},
|
|
100
|
+
}
|
mrpd/core/registry.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from mrpd.core.defaults import MRP_BOOTSTRAP_REGISTRY_RAW, MRP_DEFAULT_REGISTRY_BASE
|
|
11
|
+
from mrpd.core.models import RegistryEntry, RegistryQueryResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize_manifest_endpoints(manifest: dict, manifest_url: str) -> dict:
|
|
15
|
+
"""Ensure manifest.endpoints contain absolute URLs.
|
|
16
|
+
|
|
17
|
+
If endpoints are relative ("/mrp/execute"), prefix with the origin of manifest_url.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
out = dict(manifest)
|
|
21
|
+
endpoints = dict(out.get("endpoints") or {})
|
|
22
|
+
|
|
23
|
+
parsed = urlparse(manifest_url)
|
|
24
|
+
origin = f"{parsed.scheme}://{parsed.netloc}" if parsed.scheme and parsed.netloc else ""
|
|
25
|
+
|
|
26
|
+
for k, v in list(endpoints.items()):
|
|
27
|
+
if isinstance(v, str) and v.startswith("/") and origin:
|
|
28
|
+
endpoints[k] = origin + v
|
|
29
|
+
|
|
30
|
+
out["endpoints"] = endpoints
|
|
31
|
+
return out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RegistryClient:
|
|
35
|
+
def __init__(self, base_url: str = MRP_DEFAULT_REGISTRY_BASE, timeout: float = 10.0) -> None:
|
|
36
|
+
self.base_url = base_url.rstrip("/")
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
|
|
39
|
+
async def query(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
capability: Optional[str] = None,
|
|
43
|
+
policy: Optional[str] = None,
|
|
44
|
+
limit: int = 50,
|
|
45
|
+
cursor: Optional[str] = None,
|
|
46
|
+
) -> RegistryQueryResponse:
|
|
47
|
+
"""Query the MRP registry API. Falls back to raw GitHub JSON if API is unavailable."""
|
|
48
|
+
|
|
49
|
+
url = f"{self.base_url}/mrp/registry/query"
|
|
50
|
+
params = {"limit": str(limit)}
|
|
51
|
+
if capability:
|
|
52
|
+
params["capability"] = capability
|
|
53
|
+
if policy:
|
|
54
|
+
params["policy"] = policy
|
|
55
|
+
if cursor:
|
|
56
|
+
params["cursor"] = cursor
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=False) as client:
|
|
60
|
+
r = await client.get(url, params=params, headers={"Accept": "application/json"})
|
|
61
|
+
r.raise_for_status()
|
|
62
|
+
return RegistryQueryResponse.model_validate(r.json())
|
|
63
|
+
except Exception:
|
|
64
|
+
entries = await self._fetch_raw_entries()
|
|
65
|
+
if capability:
|
|
66
|
+
entries = [e for e in entries if capability in e.capabilities]
|
|
67
|
+
if policy:
|
|
68
|
+
entries = [e for e in entries if policy in e.policies]
|
|
69
|
+
return RegistryQueryResponse(results=entries)
|
|
70
|
+
|
|
71
|
+
async def _fetch_raw_entries(self) -> list[RegistryEntry]:
|
|
72
|
+
raw_url = os.getenv("MRP_BOOTSTRAP_REGISTRY_RAW") or MRP_BOOTSTRAP_REGISTRY_RAW
|
|
73
|
+
if not raw_url:
|
|
74
|
+
# No bootstrap configured; callers should rely on the hosted registry API.
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
if raw_url.startswith("file://"):
|
|
78
|
+
path = raw_url[len("file://") :]
|
|
79
|
+
# Windows file URLs sometimes come through as /C:/...
|
|
80
|
+
if len(path) >= 3 and path[0] == "/" and path[2] == ":":
|
|
81
|
+
path = path[1:]
|
|
82
|
+
payload = json.loads(open(path, "r", encoding="utf-8").read())
|
|
83
|
+
else:
|
|
84
|
+
async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=False) as client:
|
|
85
|
+
r = await client.get(raw_url, headers={"Accept": "application/json"})
|
|
86
|
+
r.raise_for_status()
|
|
87
|
+
payload = r.json()
|
|
88
|
+
|
|
89
|
+
if not isinstance(payload, list):
|
|
90
|
+
raise ValueError("bootstrap registry did not return a list")
|
|
91
|
+
|
|
92
|
+
entries: list[RegistryEntry] = []
|
|
93
|
+
for item in payload:
|
|
94
|
+
if not isinstance(item, dict) or not item.get("manifest_url"):
|
|
95
|
+
continue
|
|
96
|
+
entries.append(RegistryEntry.model_validate(item))
|
|
97
|
+
|
|
98
|
+
dedup: dict[str, RegistryEntry] = {}
|
|
99
|
+
for e in entries:
|
|
100
|
+
if e.id not in dedup:
|
|
101
|
+
dedup[e.id] = e
|
|
102
|
+
return list(dedup.values())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def fetch_manifest(manifest_url: str, timeout: float = 10.0) -> dict:
|
|
106
|
+
if manifest_url.startswith("file://"):
|
|
107
|
+
path = manifest_url[len("file://") :]
|
|
108
|
+
if len(path) >= 3 and path[0] == "/" and path[2] == ":":
|
|
109
|
+
path = path[1:]
|
|
110
|
+
return json.loads(open(path, "r", encoding="utf-8").read())
|
|
111
|
+
|
|
112
|
+
async with httpx.AsyncClient(timeout=timeout, follow_redirects=False) as client:
|
|
113
|
+
r = await client.get(manifest_url, headers={"Accept": "application/mrp-manifest+json, application/json"})
|
|
114
|
+
r.raise_for_status()
|
|
115
|
+
return r.json()
|
mrpd/core/schema.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from importlib import resources
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import jsonschema
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_schema_text(rel_path: str) -> str:
|
|
11
|
+
"""Load a schema file shipped inside mrpd (package data)."""
|
|
12
|
+
# rel_path like: "schemas/envelope.schema.json"
|
|
13
|
+
return resources.files("mrpd.spec").joinpath(rel_path).read_text(encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_schema(rel_path: str) -> dict[str, Any]:
|
|
17
|
+
return json.loads(load_schema_text(rel_path))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_envelope(envelope: dict[str, Any]) -> None:
|
|
21
|
+
schema = load_schema("schemas/envelope.schema.json")
|
|
22
|
+
|
|
23
|
+
# Resolve local $refs inside the shipped schema folder.
|
|
24
|
+
base = resources.files("mrpd.spec").joinpath("schemas")
|
|
25
|
+
|
|
26
|
+
def _load_by_rel(rel: str) -> dict[str, Any]:
|
|
27
|
+
p = rel
|
|
28
|
+
if p.startswith("./"):
|
|
29
|
+
p = p[2:]
|
|
30
|
+
text = base.joinpath(p).read_text(encoding="utf-8")
|
|
31
|
+
return json.loads(text)
|
|
32
|
+
|
|
33
|
+
def _https_handler(uri: str) -> Any:
|
|
34
|
+
"""Map https://moltrouter.dev/schemas/mrp/0.1/... to bundled files.
|
|
35
|
+
|
|
36
|
+
jsonschema will resolve relative refs against the envelope $id (https://moltrouter.dev/...).
|
|
37
|
+
Without this handler, it tries to fetch over the network.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
marker = "/schemas/mrp/0.1/"
|
|
41
|
+
if marker in uri:
|
|
42
|
+
rel = uri.split(marker, 1)[1]
|
|
43
|
+
return _load_by_rel(rel)
|
|
44
|
+
# Back-compat for older ids that used moltrouter.dev without the /schemas/mrp/0.1 prefix
|
|
45
|
+
uri2 = uri.replace("https://moltrouter.dev/", "https://www.moltrouter.dev/")
|
|
46
|
+
if marker in uri2:
|
|
47
|
+
rel = uri2.split(marker, 1)[1]
|
|
48
|
+
return _load_by_rel(rel)
|
|
49
|
+
# Fallback: try to treat the URL path as a relative file
|
|
50
|
+
# e.g. https://example/x/y.json -> y.json
|
|
51
|
+
rel = uri.rsplit("/", 1)[-1]
|
|
52
|
+
return _load_by_rel(rel)
|
|
53
|
+
|
|
54
|
+
# Handlers are selected by URI scheme.
|
|
55
|
+
resolver = jsonschema.RefResolver.from_schema(
|
|
56
|
+
schema,
|
|
57
|
+
handlers={
|
|
58
|
+
"": _load_by_rel, # relative refs
|
|
59
|
+
"https": _https_handler,
|
|
60
|
+
"http": _https_handler,
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
jsonschema.validate(instance=envelope, schema=schema, resolver=resolver)
|
mrpd/core/scoring.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from mrpd.core.models import RegistryEntry
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ScoreResult:
|
|
11
|
+
entry: RegistryEntry
|
|
12
|
+
score: float
|
|
13
|
+
required_matches: int
|
|
14
|
+
trust_score: float
|
|
15
|
+
proofs_count: int
|
|
16
|
+
reasons: tuple[str, ...]
|
|
17
|
+
missing: tuple[str, ...]
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def satisfied(self) -> bool:
|
|
21
|
+
return len(self.missing) == 0
|
|
22
|
+
|
|
23
|
+
def rank_key(self) -> tuple:
|
|
24
|
+
# Deterministic ordering: score, required matches, trust, proofs, name, id.
|
|
25
|
+
return (
|
|
26
|
+
-self.score,
|
|
27
|
+
-self.required_matches,
|
|
28
|
+
-self.trust_score,
|
|
29
|
+
-self.proofs_count,
|
|
30
|
+
(self.entry.name or "").lower(),
|
|
31
|
+
self.entry.id,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def score_entry(entry: RegistryEntry, *, capability: str | None, policy: str | None) -> ScoreResult:
|
|
36
|
+
"""Deterministic scoring with explicit tie-breakers.
|
|
37
|
+
|
|
38
|
+
Score weights are simple and predictable; ties break by required matches,
|
|
39
|
+
trust score, proof count, then stable name/id ordering.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
score = 0.0
|
|
43
|
+
reasons: list[str] = []
|
|
44
|
+
missing: list[str] = []
|
|
45
|
+
required_matches = 0
|
|
46
|
+
|
|
47
|
+
if capability:
|
|
48
|
+
if capability in entry.capabilities:
|
|
49
|
+
score += 50.0
|
|
50
|
+
required_matches += 1
|
|
51
|
+
reasons.append(f"capability match: {capability}")
|
|
52
|
+
else:
|
|
53
|
+
missing.append(f"capability:{capability}")
|
|
54
|
+
|
|
55
|
+
if policy:
|
|
56
|
+
if policy in entry.policies:
|
|
57
|
+
score += 20.0
|
|
58
|
+
required_matches += 1
|
|
59
|
+
reasons.append(f"policy match: {policy}")
|
|
60
|
+
else:
|
|
61
|
+
missing.append(f"policy:{policy}")
|
|
62
|
+
|
|
63
|
+
trust_score = 0.0
|
|
64
|
+
if entry.trust and entry.trust.score is not None:
|
|
65
|
+
trust_score = float(entry.trust.score)
|
|
66
|
+
score += 10.0 * trust_score
|
|
67
|
+
reasons.append(f"trust score: {trust_score:.2f}")
|
|
68
|
+
|
|
69
|
+
proofs_count = len(entry.proofs)
|
|
70
|
+
if proofs_count:
|
|
71
|
+
reasons.append(f"proofs: {proofs_count}")
|
|
72
|
+
|
|
73
|
+
if capability and policy and (capability in entry.capabilities) and (policy in entry.policies):
|
|
74
|
+
score += 5.0
|
|
75
|
+
reasons.append("capability+policy bonus")
|
|
76
|
+
|
|
77
|
+
return ScoreResult(
|
|
78
|
+
entry=entry,
|
|
79
|
+
score=score,
|
|
80
|
+
required_matches=required_matches,
|
|
81
|
+
trust_score=trust_score,
|
|
82
|
+
proofs_count=proofs_count,
|
|
83
|
+
reasons=tuple(reasons),
|
|
84
|
+
missing=tuple(missing),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def rank_entries(
|
|
89
|
+
entries: Iterable[RegistryEntry], *, capability: str | None, policy: str | None
|
|
90
|
+
) -> list[ScoreResult]:
|
|
91
|
+
scored = [score_entry(e, capability=capability, policy=policy) for e in entries]
|
|
92
|
+
return sorted(scored, key=lambda r: r.rank_key())
|
mrpd/core/util.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def utc_now_rfc3339() -> str:
|
|
9
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def sha256_hex(data: bytes) -> str:
|
|
13
|
+
return hashlib.sha256(data).hexdigest()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def approx_tokens(text: str) -> int:
|
|
17
|
+
# rough heuristic: ~4 chars/token for English-ish text
|
|
18
|
+
return max(1, len(text) // 4)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def strip_html(html: str) -> str:
|
|
22
|
+
# extremely simple tag stripper for v0 (good enough for demo)
|
|
23
|
+
html = re.sub(r"<script[\s\S]*?</script>", " ", html, flags=re.IGNORECASE)
|
|
24
|
+
html = re.sub(r"<style[\s\S]*?</style>", " ", html, flags=re.IGNORECASE)
|
|
25
|
+
text = re.sub(r"<[^>]+>", " ", html)
|
|
26
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
27
|
+
return text
|
mrpd/spec/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mrp_version": "0.1",
|
|
3
|
+
"msg_id": "00000000-0000-0000-0000-000000000001",
|
|
4
|
+
"msg_type": "DISCOVER",
|
|
5
|
+
"timestamp": "2025-01-01T00:00:00Z",
|
|
6
|
+
"sender": {"id": "agent:moltbots/alpha"},
|
|
7
|
+
"payload": {
|
|
8
|
+
"intent": "extract pricing from vendor docs",
|
|
9
|
+
"inputs": [{"type": "url", "value": "https://example.com/pricing"}],
|
|
10
|
+
"constraints": {"max_cost": 0.05, "policy": ["no_pii"]},
|
|
11
|
+
"proofs_required": ["attestation"]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mrp_version": "0.1",
|
|
3
|
+
"msg_id": "00000000-0000-0000-0000-000000000003",
|
|
4
|
+
"msg_type": "ERROR",
|
|
5
|
+
"timestamp": "2025-01-01T00:00:02Z",
|
|
6
|
+
"sender": {"id": "service:clawdbots/summarize"},
|
|
7
|
+
"payload": {
|
|
8
|
+
"code": "MRP_RATE_LIMITED",
|
|
9
|
+
"message": "Rate limit exceeded",
|
|
10
|
+
"retryable": true,
|
|
11
|
+
"retry_after_ms": 5000
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mrp_version": "0.1",
|
|
3
|
+
"msg_id": "00000000-0000-0000-0000-000000000002",
|
|
4
|
+
"msg_type": "OFFER",
|
|
5
|
+
"timestamp": "2025-01-01T00:00:01Z",
|
|
6
|
+
"sender": {"id": "service:clawdbots/summarize"},
|
|
7
|
+
"receiver": {"id": "agent:moltbots/alpha"},
|
|
8
|
+
"payload": {
|
|
9
|
+
"offers": [
|
|
10
|
+
{
|
|
11
|
+
"route_id": "route-123",
|
|
12
|
+
"capability": "summarize",
|
|
13
|
+
"confidence": 0.9,
|
|
14
|
+
"policy": ["no_pii"],
|
|
15
|
+
"risk": {"data_retention_days": 0, "training_use": "none", "subprocessors": []},
|
|
16
|
+
"endpoint": "/mrp/negotiate"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://www.moltrouter.dev/schemas/mrp/0.1/envelope.schema.json",
|
|
4
|
+
"title": "MRP Envelope (0.1)",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["mrp_version", "msg_id", "msg_type", "timestamp", "sender", "payload"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"mrp_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+$"},
|
|
10
|
+
"msg_id": {"type": "string", "minLength": 8},
|
|
11
|
+
"msg_type": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"enum": [
|
|
14
|
+
"HELLO",
|
|
15
|
+
"DISCOVER",
|
|
16
|
+
"OFFER",
|
|
17
|
+
"NEGOTIATE",
|
|
18
|
+
"EXECUTE",
|
|
19
|
+
"EVIDENCE",
|
|
20
|
+
"JOB_ACCEPTED",
|
|
21
|
+
"JOB_STATUS",
|
|
22
|
+
"STREAM_CHUNK",
|
|
23
|
+
"ERROR"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"timestamp": {"type": "string", "format": "date-time"},
|
|
27
|
+
"sender": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"required": ["id"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"id": {"type": "string", "minLength": 3},
|
|
33
|
+
"proofs": {"type": "array", "items": {"type": "string"}}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"receiver": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"required": ["id"],
|
|
40
|
+
"properties": {"id": {"type": "string", "minLength": 3}}
|
|
41
|
+
},
|
|
42
|
+
"in_reply_to": {"type": "string", "minLength": 8},
|
|
43
|
+
"trace": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"properties": {
|
|
47
|
+
"root": {"type": "string"},
|
|
48
|
+
"parent": {"type": "string"}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"auth": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": true,
|
|
54
|
+
"properties": {
|
|
55
|
+
"type": {"type": "string", "enum": ["bearer", "mTLS", "signed"]},
|
|
56
|
+
"token": {"type": "string"},
|
|
57
|
+
"scopes": {"type": "array", "items": {"type": "string"}},
|
|
58
|
+
"issuer": {"type": "string"},
|
|
59
|
+
"signature": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"additionalProperties": false,
|
|
62
|
+
"required": ["alg", "key_id", "value"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"alg": {"type": "string"},
|
|
65
|
+
"key_id": {"type": "string"},
|
|
66
|
+
"value": {"type": "string"}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"nonce": {"type": "string"},
|
|
72
|
+
"expires_at": {"type": "string", "format": "date-time"},
|
|
73
|
+
"idempotency_key": {"type": "string"},
|
|
74
|
+
"payload": {"type": "object"}
|
|
75
|
+
},
|
|
76
|
+
"allOf": [
|
|
77
|
+
{
|
|
78
|
+
"if": {"properties": {"msg_type": {"const": "DISCOVER"}}},
|
|
79
|
+
"then": {"properties": {"payload": {"$ref": "./payloads/discover.schema.json"}}}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"if": {"properties": {"msg_type": {"const": "OFFER"}}},
|
|
83
|
+
"then": {"properties": {"payload": {"$ref": "./payloads/offer.schema.json"}}}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"if": {"properties": {"msg_type": {"const": "NEGOTIATE"}}},
|
|
87
|
+
"then": {"properties": {"payload": {"$ref": "./payloads/negotiate.schema.json"}}}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"if": {"properties": {"msg_type": {"const": "EXECUTE"}}},
|
|
91
|
+
"then": {"properties": {"payload": {"$ref": "./payloads/execute.schema.json"}}}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"if": {"properties": {"msg_type": {"const": "EVIDENCE"}}},
|
|
95
|
+
"then": {"properties": {"payload": {"$ref": "./payloads/evidence.schema.json"}}}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"if": {"properties": {"msg_type": {"const": "ERROR"}}},
|
|
99
|
+
"then": {"properties": {"payload": {"$ref": "./payloads/error.schema.json"}}}
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://www.moltrouter.dev/schemas/mrp/0.1/manifest.schema.json",
|
|
4
|
+
"title": "MRP Capability Manifest (0.1)",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["capability_id", "capability", "version", "inputs", "outputs", "endpoints"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"capability_id": {"type": "string"},
|
|
10
|
+
"capability": {"type": "string"},
|
|
11
|
+
"version": {"type": "string"},
|
|
12
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
13
|
+
"inputs": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"required": ["type"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"type": {"type": "string"},
|
|
21
|
+
"schema_ref": {"type": "string"}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"outputs": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"required": ["type"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"type": {"type": "string"},
|
|
33
|
+
"schema_ref": {"type": "string"}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"constraints": {"type": "object", "additionalProperties": true},
|
|
38
|
+
"cost": {"type": "object"},
|
|
39
|
+
"latency": {"type": "object"},
|
|
40
|
+
"proofs_required": {"type": "array", "items": {"type": "string"}},
|
|
41
|
+
"endpoints": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": ["discover", "negotiate", "execute"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"discover": {"type": "string"},
|
|
47
|
+
"negotiate": {"type": "string"},
|
|
48
|
+
"execute": {"type": "string"}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://www.moltrouter.dev/schemas/mrp/0.1/payloads/discover.schema.json",
|
|
4
|
+
"title": "MRP DISCOVER Payload",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["intent"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"intent": {"type": "string", "minLength": 1},
|
|
10
|
+
"inputs": {"type": "array", "items": {"$ref": "../types/input.schema.json"}},
|
|
11
|
+
"constraints": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"additionalProperties": true,
|
|
14
|
+
"properties": {
|
|
15
|
+
"max_cost": {"type": "number", "minimum": 0},
|
|
16
|
+
"max_latency_ms": {"type": "integer", "minimum": 0},
|
|
17
|
+
"data_residency": {"type": "string"},
|
|
18
|
+
"policy": {"type": "array", "items": {"type": "string"}}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"proofs_required": {"type": "array", "items": {"type": "string"}}
|
|
22
|
+
}
|
|
23
|
+
}
|