agentprov-capture 0.1.1__tar.gz
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.
- agentprov_capture-0.1.1/PKG-INFO +24 -0
- agentprov_capture-0.1.1/README.md +11 -0
- agentprov_capture-0.1.1/pyproject.toml +20 -0
- agentprov_capture-0.1.1/pyproject.toml.orig +18 -0
- agentprov_capture-0.1.1/src/agentprov_capture/__init__.py +5 -0
- agentprov_capture-0.1.1/src/agentprov_capture/endpoints.py +252 -0
- agentprov_capture-0.1.1/src/agentprov_capture/endpoints.toml +156 -0
- agentprov_capture-0.1.1/src/agentprov_capture/envstate.py +182 -0
- agentprov_capture-0.1.1/src/agentprov_capture/errors.py +37 -0
- agentprov_capture-0.1.1/src/agentprov_capture/events.py +196 -0
- agentprov_capture-0.1.1/src/agentprov_capture/log.py +536 -0
- agentprov_capture-0.1.1/src/agentprov_capture/py.typed +0 -0
- agentprov_capture-0.1.1/src/agentprov_capture/redact.py +695 -0
- agentprov_capture-0.1.1/src/agentprov_capture/redact_allowlist.toml +52 -0
- agentprov_capture-0.1.1/src/agentprov_capture/rules/cap.rules.toml +121 -0
- agentprov_capture-0.1.1/src/agentprov_capture/verify.py +377 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentprov-capture
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: AgentPROV capture/0.1 raw-evidence event log: model, writer, hash-chain verifier, redaction. Stdlib + rfc8785 only; shared by the AgentPROV core (reader) and the isolated live backends (writer).
|
|
5
|
+
Author: Nicholas Jones
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Dist: rfc8785>=0.1.4
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Project-URL: Homepage, https://w3id.org/agentprov/
|
|
10
|
+
Project-URL: Documentation, https://w3id.org/agentprov/docs/
|
|
11
|
+
Project-URL: Source, https://github.com/nickjones39/AgentPROV
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# agentprov-capture
|
|
15
|
+
|
|
16
|
+
The `capture/0.1` raw-evidence event log of [AgentPROV](https://w3id.org/agentprov/): its model, the writer that live
|
|
17
|
+
backends use to record an agent run, the hash-chain verifier and the redaction of credentials. Standard library plus
|
|
18
|
+
`rfc8785` only, so it installs in the isolated backend environments (Python 3.12 and later) as well as in the core.
|
|
19
|
+
|
|
20
|
+
You normally get it as a dependency of [`agentprov`](https://pypi.org/project/agentprov/). The format is specified in
|
|
21
|
+
the AgentPROV repository (`docs/architecture/`, <https://github.com/nickjones39/AgentPROV>), and the user guide is at
|
|
22
|
+
<https://w3id.org/agentprov/docs/>.
|
|
23
|
+
|
|
24
|
+
MIT licence.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# agentprov-capture
|
|
2
|
+
|
|
3
|
+
The `capture/0.1` raw-evidence event log of [AgentPROV](https://w3id.org/agentprov/): its model, the writer that live
|
|
4
|
+
backends use to record an agent run, the hash-chain verifier and the redaction of credentials. Standard library plus
|
|
5
|
+
`rfc8785` only, so it installs in the isolated backend environments (Python 3.12 and later) as well as in the core.
|
|
6
|
+
|
|
7
|
+
You normally get it as a dependency of [`agentprov`](https://pypi.org/project/agentprov/). The format is specified in
|
|
8
|
+
the AgentPROV repository (`docs/architecture/`, <https://github.com/nickjones39/AgentPROV>), and the user guide is at
|
|
9
|
+
<https://w3id.org/agentprov/docs/>.
|
|
10
|
+
|
|
11
|
+
MIT licence.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agentprov-capture"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "AgentPROV capture/0.1 raw-evidence event log: model, writer, hash-chain verifier, redaction. Stdlib + rfc8785 only; shared by the AgentPROV core (reader) and the isolated live backends (writer)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
dependencies = ["rfc8785>=0.1.4"]
|
|
9
|
+
|
|
10
|
+
[[project.authors]]
|
|
11
|
+
name = "Nicholas Jones"
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
Homepage = "https://w3id.org/agentprov/"
|
|
15
|
+
Documentation = "https://w3id.org/agentprov/docs/"
|
|
16
|
+
Source = "https://github.com/nickjones39/AgentPROV"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.12.18,<0.13"]
|
|
20
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agentprov-capture"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "AgentPROV capture/0.1 raw-evidence event log: model, writer, hash-chain verifier, redaction. Stdlib + rfc8785 only; shared by the AgentPROV core (reader) and the isolated live backends (writer)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12" # live backends run Python 3.12 (AgentDojo 0.1.35 baseline parity)
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Nicholas Jones" }]
|
|
9
|
+
dependencies = ["rfc8785>=0.1.4"]
|
|
10
|
+
|
|
11
|
+
[project.urls]
|
|
12
|
+
Homepage = "https://w3id.org/agentprov/"
|
|
13
|
+
Documentation = "https://w3id.org/agentprov/docs/"
|
|
14
|
+
Source = "https://github.com/nickjones39/AgentPROV"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.12.18,<0.13"]
|
|
18
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""agentprov_capture: the capture/0.1 raw-evidence event log shared by the AgentPROV core (reader and
|
|
2
|
+
verifier) and the isolated live backends (writer). Stdlib + rfc8785 only, Python >= 3.12."""
|
|
3
|
+
|
|
4
|
+
FORMAT = "capture/0.1"
|
|
5
|
+
__version__ = "0.1.1" # equals capture/pyproject.toml (tests/unit/test_versions_source.py)
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""The endpoint registry: ``endpoints.toml`` as validated data (MN M.1.10 item 2; N.2.1, N.2.1a; G.9.2).
|
|
2
|
+
|
|
3
|
+
Stdlib only (``tomllib``; Python >= 3.12). The registry names environment variables, never values. Rules enforced here:
|
|
4
|
+
|
|
5
|
+
* a base URL is https, except loopback (``127.0.0.1``, ``[::1]``, ``localhost``);
|
|
6
|
+
* a base URL has no userinfo, query or fragment (a key could hide in them; G.6.4);
|
|
7
|
+
* a ``local`` entry is loopback and names its ``server`` kind (D-690). Its ``api_key_env`` is optional: a server set up
|
|
8
|
+
with an API key (oMLX's, D-827) gets it as a bearer header when the key is set, and none otherwise. A non-local entry
|
|
9
|
+
names ``api_key_env`` and has no ``server``;
|
|
10
|
+
* a custom base URL's host must equal the registered host unless the caller passes ``allow_custom=True``
|
|
11
|
+
(the CLI's ``--allow-custom-endpoint``, recorded in the manifest);
|
|
12
|
+
* ``api`` names the wire format the endpoint speaks: ``openai_chat`` (the default; OpenAI-compatible chat completions)
|
|
13
|
+
or ``anthropic_messages`` (the Anthropic Messages API; D-656).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ipaddress
|
|
19
|
+
import re
|
|
20
|
+
import tomllib
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from importlib import resources
|
|
23
|
+
from typing import Any, Final
|
|
24
|
+
from urllib.parse import urlsplit
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"API_KINDS",
|
|
28
|
+
"LOCAL_SERVERS",
|
|
29
|
+
"LOOPBACK_HOSTS",
|
|
30
|
+
"Endpoint",
|
|
31
|
+
"EndpointError",
|
|
32
|
+
"check_base_url",
|
|
33
|
+
"get_endpoint",
|
|
34
|
+
"is_loopback_host",
|
|
35
|
+
"is_private_lan_host",
|
|
36
|
+
"load_endpoints",
|
|
37
|
+
"parse_registry",
|
|
38
|
+
"validate_base_url",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
#: The loopback hosts of G.9.2, as ``urllib.parse`` reports them (brackets removed from the IPv6 literal).
|
|
42
|
+
LOOPBACK_HOSTS: Final = frozenset({"127.0.0.1", "::1", "localhost"})
|
|
43
|
+
|
|
44
|
+
#: The wire formats an endpoint may speak (``api``); the first is the default.
|
|
45
|
+
API_KINDS: Final = ("openai_chat", "anthropic_messages")
|
|
46
|
+
#: The local server kinds of D-690. ``server`` selects the discovery and fingerprint probes of ``agentprov models``.
|
|
47
|
+
LOCAL_SERVERS: Final = frozenset({"ollama", "omlx", "lmstudio", "mlx_lm", "llamacpp", "vllm"})
|
|
48
|
+
|
|
49
|
+
_ENV_NAME: Final = re.compile(r"[A-Z][A-Z0-9_]{0,63}")
|
|
50
|
+
_ENDPOINT_NAME: Final = re.compile(r"[a-z][a-z0-9_-]{0,31}")
|
|
51
|
+
_HEADER_NAME: Final = re.compile(r"[a-z0-9][a-z0-9-]{0,63}")
|
|
52
|
+
_ENTRY_KEYS: Final = frozenset(
|
|
53
|
+
{
|
|
54
|
+
"base_url",
|
|
55
|
+
"base_url_env",
|
|
56
|
+
"api_key_env",
|
|
57
|
+
"model_env",
|
|
58
|
+
"local",
|
|
59
|
+
"server_max_concurrency",
|
|
60
|
+
"request_headers",
|
|
61
|
+
"response_headers",
|
|
62
|
+
"api",
|
|
63
|
+
"server",
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EndpointError(ValueError):
|
|
69
|
+
"""A registry or base-URL violation. ``code`` is stable; messages never carry values from the environment."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, code: str, message: str) -> None:
|
|
72
|
+
super().__init__(f"{code}: {message}")
|
|
73
|
+
self.code = code
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True, slots=True)
|
|
77
|
+
class Endpoint:
|
|
78
|
+
"""One registry entry. ``*_env`` fields are environment-variable NAMES (or None), never values."""
|
|
79
|
+
|
|
80
|
+
name: str
|
|
81
|
+
base_url: str
|
|
82
|
+
host: str
|
|
83
|
+
base_url_env: str | None
|
|
84
|
+
api_key_env: str | None
|
|
85
|
+
model_env: str | None
|
|
86
|
+
local: bool
|
|
87
|
+
server_max_concurrency: int | None
|
|
88
|
+
#: header NAMES (lower case) whose values wire-capture redaction may record besides the global allowlist
|
|
89
|
+
#: (``redact_allowlist.toml``): for example an API-version header (request) or a request-id header (response).
|
|
90
|
+
#: The hard denylist of G.6.4 wins over them.
|
|
91
|
+
request_headers: tuple[str, ...] = ()
|
|
92
|
+
response_headers: tuple[str, ...] = ()
|
|
93
|
+
#: the wire format (one of :data:`API_KINDS`): which LLM element and recorder the harness uses (D-656)
|
|
94
|
+
api: str = API_KINDS[0]
|
|
95
|
+
#: the local server kind (one of :data:`LOCAL_SERVERS`) for a ``local`` entry; ``None`` for a remote one (D-690)
|
|
96
|
+
server: str | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_loopback_host(host: str) -> bool:
|
|
100
|
+
return host.lower() in LOOPBACK_HOSTS
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
#: The private ranges a LAN endpoint may use (D-698): RFC 1918, the shared address space of RFC 6598 (CGNAT, used by
|
|
104
|
+
#: Tailscale-style overlays), and IPv6 unique local addresses. Link-local, loopback and every public range are not.
|
|
105
|
+
LAN_NETWORKS: Final = tuple(ipaddress.ip_network(n) for n in (
|
|
106
|
+
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10", "fc00::/7"))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_private_lan_host(host: str) -> bool:
|
|
110
|
+
"""A literal IP address in :data:`LAN_NETWORKS` (never a name: a name can be made to resolve anywhere), without an
|
|
111
|
+
IPv6 zone id (``%en0`` names an interface of this host, not a LAN peer)."""
|
|
112
|
+
if "%" in host:
|
|
113
|
+
return False
|
|
114
|
+
try:
|
|
115
|
+
address = ipaddress.ip_address(host)
|
|
116
|
+
except ValueError:
|
|
117
|
+
return False
|
|
118
|
+
return any(address in net for net in LAN_NETWORKS if net.version == address.version)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def validate_base_url(base_url: str, *, allow_lan: bool = False) -> str:
|
|
122
|
+
"""Check the G.9.2 / G.6.4 rules on ``base_url`` and return its host (lower case).
|
|
123
|
+
|
|
124
|
+
http is allowed on a loopback host, and, with ``allow_lan`` (the CLI's ``--allow-lan-endpoint``, D-698), on a
|
|
125
|
+
literal private address. Raises :class:`EndpointError` (``endpoint.url.*``). Nothing is fetched or resolved.
|
|
126
|
+
"""
|
|
127
|
+
if not isinstance(base_url, str) or not base_url or any(ord(c) < 0x21 or ord(c) == 0x7F for c in base_url):
|
|
128
|
+
raise EndpointError("endpoint.url.malformed", "base URL is empty or contains whitespace or control characters")
|
|
129
|
+
try:
|
|
130
|
+
parts = urlsplit(base_url)
|
|
131
|
+
host = parts.hostname
|
|
132
|
+
parts.port # noqa: B018 - raises ValueError on a bad port
|
|
133
|
+
except ValueError:
|
|
134
|
+
raise EndpointError("endpoint.url.malformed", "base URL cannot be parsed") from None
|
|
135
|
+
if parts.scheme not in ("http", "https") or not host:
|
|
136
|
+
raise EndpointError("endpoint.url.malformed", "base URL needs an http(s) scheme and a host")
|
|
137
|
+
if parts.username is not None or parts.password is not None or "@" in parts.netloc:
|
|
138
|
+
raise EndpointError("endpoint.url.userinfo", "base URL must not carry userinfo")
|
|
139
|
+
if parts.query or "?" in base_url:
|
|
140
|
+
raise EndpointError("endpoint.url.query", "base URL must not carry a query string")
|
|
141
|
+
if parts.fragment or "#" in base_url:
|
|
142
|
+
raise EndpointError("endpoint.url.fragment", "base URL must not carry a fragment")
|
|
143
|
+
host = host.lower()
|
|
144
|
+
if parts.scheme != "https" and not is_loopback_host(host) and not (allow_lan and is_private_lan_host(host)):
|
|
145
|
+
raise EndpointError("endpoint.url.not_https", "base URL must be https unless the host is loopback")
|
|
146
|
+
return host
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def check_base_url(endpoint: Endpoint, base_url: str, *, allow_custom: bool = False) -> bool:
|
|
150
|
+
"""Validate a base URL given for ``endpoint`` (e.g. from its ``base_url_env``). Returns True when it is a custom
|
|
151
|
+
endpoint (its host differs from the registered one), which the caller must record in the manifest.
|
|
152
|
+
|
|
153
|
+
The https/loopback/userinfo/query rules always apply. A host mismatch raises ``endpoint.url.host_mismatch``
|
|
154
|
+
unless ``allow_custom`` (the CLI's ``--allow-custom-endpoint``, R-39).
|
|
155
|
+
"""
|
|
156
|
+
host = validate_base_url(base_url)
|
|
157
|
+
if host == endpoint.host:
|
|
158
|
+
return False
|
|
159
|
+
if not allow_custom:
|
|
160
|
+
raise EndpointError("endpoint.url.host_mismatch", f"host does not match the registered host of {endpoint.name!r}")
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _opt_env(entry: dict[str, Any], key: str, name: str) -> str | None:
|
|
165
|
+
value = entry.get(key)
|
|
166
|
+
if value is None:
|
|
167
|
+
return None
|
|
168
|
+
if not isinstance(value, str) or not _ENV_NAME.fullmatch(value):
|
|
169
|
+
raise EndpointError("endpoint.registry.env_name", f"endpoint {name!r}: {key} must be an upper-case variable name")
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _opt_headers(entry: dict[str, Any], key: str, name: str) -> tuple[str, ...]:
|
|
174
|
+
value = entry.get(key, [])
|
|
175
|
+
if not isinstance(value, list) or not all(isinstance(h, str) and _HEADER_NAME.fullmatch(h) for h in value):
|
|
176
|
+
raise EndpointError("endpoint.registry.header", f"endpoint {name!r}: {key} must be a list of lower-case header names")
|
|
177
|
+
return tuple(sorted(set(value)))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def parse_registry(data: dict[str, Any]) -> dict[str, Endpoint]:
|
|
181
|
+
"""Validate a parsed ``endpoints.toml`` document and return the entries by name, sorted."""
|
|
182
|
+
if data.get("version") != 1:
|
|
183
|
+
raise EndpointError("endpoint.registry.version", "unsupported registry version")
|
|
184
|
+
entries = data.get("endpoints")
|
|
185
|
+
if not isinstance(entries, dict) or not entries:
|
|
186
|
+
raise EndpointError("endpoint.registry.empty", "registry has no [endpoints.*] tables")
|
|
187
|
+
out: dict[str, Endpoint] = {}
|
|
188
|
+
for name in sorted(entries):
|
|
189
|
+
entry = entries[name]
|
|
190
|
+
if not _ENDPOINT_NAME.fullmatch(name) or not isinstance(entry, dict):
|
|
191
|
+
raise EndpointError("endpoint.registry.name", "bad endpoint name or entry")
|
|
192
|
+
unknown = set(entry) - _ENTRY_KEYS
|
|
193
|
+
if unknown:
|
|
194
|
+
raise EndpointError("endpoint.registry.key", f"endpoint {name!r}: unknown key {sorted(unknown)[0]!r}")
|
|
195
|
+
base_url = entry.get("base_url")
|
|
196
|
+
if not isinstance(base_url, str):
|
|
197
|
+
raise EndpointError("endpoint.registry.base_url", f"endpoint {name!r}: base_url is required")
|
|
198
|
+
host = validate_base_url(base_url)
|
|
199
|
+
local = entry.get("local", False)
|
|
200
|
+
if not isinstance(local, bool):
|
|
201
|
+
raise EndpointError("endpoint.registry.local", f"endpoint {name!r}: local must be a boolean")
|
|
202
|
+
if local and not is_loopback_host(host):
|
|
203
|
+
raise EndpointError("endpoint.registry.local", f"endpoint {name!r}: a local entry must be loopback")
|
|
204
|
+
api_key_env = _opt_env(entry, "api_key_env", name)
|
|
205
|
+
if not local and api_key_env is None:
|
|
206
|
+
raise EndpointError("endpoint.registry.api_key_env", f"endpoint {name!r}: api_key_env is required")
|
|
207
|
+
server = entry.get("server")
|
|
208
|
+
if local and (not isinstance(server, str) or server not in LOCAL_SERVERS):
|
|
209
|
+
raise EndpointError("endpoint.registry.server", f"endpoint {name!r}: a local entry needs server, one of "
|
|
210
|
+
f"{sorted(LOCAL_SERVERS)}")
|
|
211
|
+
if not local and server is not None:
|
|
212
|
+
raise EndpointError("endpoint.registry.server", f"endpoint {name!r}: server belongs to local entries")
|
|
213
|
+
conc = entry.get("server_max_concurrency")
|
|
214
|
+
if conc is not None and (isinstance(conc, bool) or not isinstance(conc, int) or conc < 1):
|
|
215
|
+
raise EndpointError(
|
|
216
|
+
"endpoint.registry.concurrency", f"endpoint {name!r}: server_max_concurrency must be an integer >= 1"
|
|
217
|
+
)
|
|
218
|
+
api = entry.get("api", API_KINDS[0])
|
|
219
|
+
if api not in API_KINDS:
|
|
220
|
+
raise EndpointError("endpoint.registry.api", f"endpoint {name!r}: api must be one of {list(API_KINDS)}")
|
|
221
|
+
if local and api != API_KINDS[0]: # every local server kind of D-690 speaks the OpenAI chat wire
|
|
222
|
+
raise EndpointError("endpoint.registry.local_api", f"endpoint {name!r}: a local entry speaks {API_KINDS[0]}")
|
|
223
|
+
out[name] = Endpoint(
|
|
224
|
+
name=name,
|
|
225
|
+
base_url=base_url,
|
|
226
|
+
host=host,
|
|
227
|
+
base_url_env=_opt_env(entry, "base_url_env", name),
|
|
228
|
+
api_key_env=api_key_env,
|
|
229
|
+
model_env=_opt_env(entry, "model_env", name),
|
|
230
|
+
local=local,
|
|
231
|
+
server_max_concurrency=conc,
|
|
232
|
+
request_headers=_opt_headers(entry, "request_headers", name),
|
|
233
|
+
response_headers=_opt_headers(entry, "response_headers", name),
|
|
234
|
+
api=api,
|
|
235
|
+
server=server,
|
|
236
|
+
)
|
|
237
|
+
return out
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def load_endpoints() -> dict[str, Endpoint]:
|
|
241
|
+
"""Load and validate the packaged ``endpoints.toml`` (read through ``importlib.resources``)."""
|
|
242
|
+
raw = resources.files("agentprov_capture").joinpath("endpoints.toml").read_bytes()
|
|
243
|
+
return parse_registry(tomllib.loads(raw.decode("utf-8")))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def get_endpoint(name: str) -> Endpoint:
|
|
247
|
+
"""The registry entry ``name`` (``EndpointError("endpoint.unknown")`` if absent)."""
|
|
248
|
+
registry = load_endpoints()
|
|
249
|
+
try:
|
|
250
|
+
return registry[name]
|
|
251
|
+
except KeyError:
|
|
252
|
+
raise EndpointError("endpoint.unknown", f"unknown endpoint shorthand; known: {sorted(registry)}") from None
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# AgentPROV endpoint registry (MN M.1.10 item 2; N.2.1 `--model`; N.2.1a; G.9.2).
|
|
2
|
+
# Data, not code: read through importlib.resources by agentprov_capture.endpoints (no __init__.py needed here).
|
|
3
|
+
#
|
|
4
|
+
# Names only: `base_url_env`, `api_key_env` and `model_env` name environment variables (the .env keys); no value is
|
|
5
|
+
# ever stored here. There are no default model ids (Q5, closed): a model id comes from `--model-id` or `model_env`.
|
|
6
|
+
# `base_url` is the public default. Its host is the registered host that any other base URL must match unless
|
|
7
|
+
# `--allow-custom-endpoint` is given (R-39). A base URL MUST be https, except loopback (127.0.0.1, [::1], localhost),
|
|
8
|
+
# and MUST carry no userinfo, query or fragment (G.6.4).
|
|
9
|
+
#
|
|
10
|
+
# `request_headers` / `response_headers` (optional, lower-case header NAMES) extend the global header allowlist of
|
|
11
|
+
# redact_allowlist.toml for that endpoint (G.6.4: "per-endpoint names declared in endpoints.toml"); the hard denylist
|
|
12
|
+
# wins over them. Only request-id style headers are declared; add a name here, with a test, when a provider needs it.
|
|
13
|
+
#
|
|
14
|
+
# `api` (optional) is the wire format: `openai_chat` (default) or `anthropic_messages` (D-656). It selects the LLM
|
|
15
|
+
# element and the wire recorder's decoding; the CLI sets the agent's provider from it.
|
|
16
|
+
#
|
|
17
|
+
# Baseline set from AgentDojo-PROV `runtime.py` (build_* functions and _LLM_BUILDERS); `omlx` is new (D-10); `lmstudio`,
|
|
18
|
+
# `mlx_lm`, `llamacpp` and `vllm` are new (D-690).
|
|
19
|
+
|
|
20
|
+
version = 1
|
|
21
|
+
|
|
22
|
+
[endpoints.deepseek]
|
|
23
|
+
base_url = "https://api.deepseek.com/v1"
|
|
24
|
+
base_url_env = "DEEPSEEK_BASE_URL"
|
|
25
|
+
api_key_env = "DEEPSEEK_API_KEY"
|
|
26
|
+
model_env = "DEEPSEEK_MODEL"
|
|
27
|
+
local = false
|
|
28
|
+
|
|
29
|
+
[endpoints.openai]
|
|
30
|
+
base_url = "https://api.openai.com/v1"
|
|
31
|
+
base_url_env = "OPENAI_BASE_URL"
|
|
32
|
+
api_key_env = "OPENAI_API_KEY"
|
|
33
|
+
model_env = "OPENAI_MODEL"
|
|
34
|
+
local = false
|
|
35
|
+
response_headers = ["x-request-id", "openai-version"]
|
|
36
|
+
|
|
37
|
+
# One OpenRouter key and base URL serve openrouter, gemini, llama, qwen and glm (baseline `_build_openrouter_llm`).
|
|
38
|
+
[endpoints.openrouter]
|
|
39
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
40
|
+
base_url_env = "OPENROUTER_BASE_URL"
|
|
41
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
42
|
+
model_env = "OPENROUTER_MODEL"
|
|
43
|
+
local = false
|
|
44
|
+
response_headers = ["x-request-id"]
|
|
45
|
+
|
|
46
|
+
[endpoints.gemini]
|
|
47
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
48
|
+
base_url_env = "OPENROUTER_BASE_URL"
|
|
49
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
50
|
+
model_env = "GEMINI_MODEL"
|
|
51
|
+
local = false
|
|
52
|
+
response_headers = ["x-request-id"]
|
|
53
|
+
|
|
54
|
+
[endpoints.llama]
|
|
55
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
56
|
+
base_url_env = "OPENROUTER_BASE_URL"
|
|
57
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
58
|
+
model_env = "LLAMA_MODEL"
|
|
59
|
+
local = false
|
|
60
|
+
response_headers = ["x-request-id"]
|
|
61
|
+
|
|
62
|
+
[endpoints.qwen]
|
|
63
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
64
|
+
base_url_env = "OPENROUTER_BASE_URL"
|
|
65
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
66
|
+
model_env = "QWEN_MODEL"
|
|
67
|
+
local = false
|
|
68
|
+
response_headers = ["x-request-id"]
|
|
69
|
+
|
|
70
|
+
[endpoints.glm]
|
|
71
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
72
|
+
base_url_env = "OPENROUTER_BASE_URL"
|
|
73
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
74
|
+
model_env = "GLM_MODEL"
|
|
75
|
+
local = false
|
|
76
|
+
response_headers = ["x-request-id"]
|
|
77
|
+
|
|
78
|
+
# The Anthropic Messages API, direct (D-656). The SDK appends /v1/messages to the base URL. Model ids are the bare API
|
|
79
|
+
# ids (claude-opus-5-5, claude-sonnet-5-5, claude-haiku-5-5, ...), never date-suffixed. Recorded header values: the
|
|
80
|
+
# API version and beta flags sent, and the request id and the two retry headers the SDK's retry decision reads. The
|
|
81
|
+
# organisation id and the rate-limit headers keep their names only; x-api-key is on the hard denylist.
|
|
82
|
+
[endpoints.anthropic]
|
|
83
|
+
base_url = "https://api.anthropic.com"
|
|
84
|
+
base_url_env = "ANTHROPIC_BASE_URL"
|
|
85
|
+
api_key_env = "ANTHROPIC_API_KEY"
|
|
86
|
+
model_env = "ANTHROPIC_MODEL"
|
|
87
|
+
local = false
|
|
88
|
+
api = "anthropic_messages"
|
|
89
|
+
request_headers = ["anthropic-beta", "anthropic-version"]
|
|
90
|
+
response_headers = ["request-id", "retry-after", "x-should-retry"]
|
|
91
|
+
|
|
92
|
+
# Local OpenAI-compatible servers (N.2.1a; D-690): http on loopback is allowed and no key is needed (an optional
|
|
93
|
+
# `api_key_env` sends one when it is set, D-827). `server` names the server kind; `agentprov models` uses it to
|
|
94
|
+
# fingerprint the server that answers (two kinds share a default port: 8000 omlx/vllm, 8080 mlx_lm/llamacpp) and to
|
|
95
|
+
# read the model identity it exposes. Base URLs are pinned to 127.0.0.1, not `localhost`, which may resolve to ::1.
|
|
96
|
+
# Tool calls: whether a model's replies carry structured `tool_calls` depends on the server, its flags and the model's
|
|
97
|
+
# template; the preflight probe decides (D.10.9 A4), and text-encoded calls need
|
|
98
|
+
# `--allow-text-tool-calls`. `server_max_concurrency` is set only where the server's default is fixed; LM Studio and
|
|
99
|
+
# llama.cpp report their own value, and `--server-max-concurrency` overrides.
|
|
100
|
+
|
|
101
|
+
# Ollama. Structured tool calls for models whose capabilities include "tools"; others are refused by the server (400).
|
|
102
|
+
# OLLAMA_NUM_PARALLEL depends on the version and memory, and is not reported, so no concurrency default is set.
|
|
103
|
+
[endpoints.ollama]
|
|
104
|
+
base_url = "http://127.0.0.1:11434/v1"
|
|
105
|
+
base_url_env = "LOCAL_LLM_BASE_URL"
|
|
106
|
+
model_env = "LOCAL_MODEL_ID"
|
|
107
|
+
local = true
|
|
108
|
+
server = "ollama"
|
|
109
|
+
|
|
110
|
+
# oMLX (MLX inference server, Apple Silicon). The model id is the oMLX model directory name or alias (`--model-id`).
|
|
111
|
+
# `server_max_concurrency` is oMLX's `--max-concurrent-requests` default. Structured tool calls through oMLX's parser.
|
|
112
|
+
# `api_key_env` is optional on a local entry (D-827): oMLX can be set up with an API key. When `OMLX_API_KEY` is set
|
|
113
|
+
# (keychain first, then .env), it is sent as a bearer header to the loopback server, and never to a LAN address; when
|
|
114
|
+
# it is not set, no key is sent, as for every other local server. `model_env` (D-827 addendum): `OMLX_MODEL` in .env
|
|
115
|
+
# names the served model id (oMLX's own id, e.g. its folder name, not a Hugging Face repo id) when `--model-id` is not
|
|
116
|
+
# given; without either, a terminal offers a pick.
|
|
117
|
+
[endpoints.omlx]
|
|
118
|
+
base_url = "http://127.0.0.1:8000/v1"
|
|
119
|
+
base_url_env = "OMLX_BASE_URL"
|
|
120
|
+
api_key_env = "OMLX_API_KEY"
|
|
121
|
+
model_env = "OMLX_MODEL"
|
|
122
|
+
local = true
|
|
123
|
+
server = "omlx"
|
|
124
|
+
server_max_concurrency = 8
|
|
125
|
+
|
|
126
|
+
# LM Studio's server (`lms server start`). Native tool calls for models with `trained_for_tool_use`; for other models LM
|
|
127
|
+
# Studio adds its own tool-use system prompt on the server (invisible to the wire capture), which `run` refuses unless
|
|
128
|
+
# `--allow-text-tool-calls`. The parallel-request limit is read from the loaded instance (`/api/v1/models`).
|
|
129
|
+
[endpoints.lmstudio]
|
|
130
|
+
base_url = "http://127.0.0.1:1234/v1"
|
|
131
|
+
base_url_env = "LMSTUDIO_BASE_URL"
|
|
132
|
+
local = true
|
|
133
|
+
server = "lmstudio"
|
|
134
|
+
|
|
135
|
+
# mlx_lm.server (MLX LM). The model id is a Hugging Face repo id or a local path. Requests are served one at a time by
|
|
136
|
+
# default. Tool calls depend on the mlx-lm version and the model's chat template; text-encoded calls are common.
|
|
137
|
+
[endpoints.mlx_lm]
|
|
138
|
+
base_url = "http://127.0.0.1:8080/v1"
|
|
139
|
+
base_url_env = "MLX_LM_BASE_URL"
|
|
140
|
+
local = true
|
|
141
|
+
server = "mlx_lm"
|
|
142
|
+
server_max_concurrency = 1
|
|
143
|
+
|
|
144
|
+
# llama.cpp `llama-server`. Structured tool calls only when started with `--jinja`; the slot count is read from /props.
|
|
145
|
+
[endpoints.llamacpp]
|
|
146
|
+
base_url = "http://127.0.0.1:8080/v1"
|
|
147
|
+
base_url_env = "LLAMACPP_BASE_URL"
|
|
148
|
+
local = true
|
|
149
|
+
server = "llamacpp"
|
|
150
|
+
|
|
151
|
+
# vLLM's OpenAI-compatible server. Structured tool calls only with `--enable-auto-tool-choice --tool-call-parser <p>`.
|
|
152
|
+
[endpoints.vllm]
|
|
153
|
+
base_url = "http://127.0.0.1:8000/v1"
|
|
154
|
+
base_url_env = "VLLM_BASE_URL"
|
|
155
|
+
local = true
|
|
156
|
+
server = "vllm"
|