webagent-protocol 0.2.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.
@@ -0,0 +1,90 @@
1
+ """Per-IP and per-agent-key rate limiting on top of a pluggable :class:`~wap.storage.StateStore`.
2
+
3
+ Each principal gets a sliding-window counter (sustained rate) and a token bucket
4
+ (bursts); see :mod:`wap.storage.meters`. A request is admitted only if every
5
+ applicable meter admits it, and meters are only charged on admission, so a
6
+ rejected request never consumes budget in another scope.
7
+
8
+ With the default :class:`~wap.storage.MemoryStore` the limits apply per process.
9
+ Pass a shared store (e.g. :class:`~wap.storage.RedisStore`) to enforce them
10
+ across every worker and machine.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import time
16
+ from collections.abc import Callable, Iterable
17
+
18
+ from ..spec.models import RateLimitPolicy
19
+ from ..storage.base import StateStore
20
+ from ..storage.memory import MemoryStore
21
+ from ..storage.meters import RateLimitDecision, SlidingWindowCounter, TokenBucket
22
+
23
+
24
+ class RateLimiter:
25
+ """Admits or rejects requests for (scope, principal) pairs under a :class:`RateLimitPolicy`."""
26
+
27
+ def __init__(
28
+ self,
29
+ policy: RateLimitPolicy | None = None,
30
+ store: StateStore | None = None,
31
+ *,
32
+ window_seconds: float = 60.0,
33
+ idle_ttl: float = 600.0,
34
+ max_keys: int = 100_000,
35
+ clock: Callable[[], float] = time.time,
36
+ ) -> None:
37
+ self.policy = policy or RateLimitPolicy()
38
+ self.window_seconds = window_seconds
39
+ self.store: StateStore = store or MemoryStore(max_rate_keys=max_keys, rate_idle_ttl=idle_ttl)
40
+ self._clock = clock
41
+
42
+ def keys_for(
43
+ self, ip: str | None, agent_key: str | None, policy: RateLimitPolicy | None = None
44
+ ) -> list[tuple[str, str]]:
45
+ policy = policy or self.policy
46
+ keys: list[tuple[str, str]] = []
47
+ if "ip" in policy.scopes and ip:
48
+ keys.append(("ip", f"ip:{ip}"))
49
+ if "agent_key" in policy.scopes and agent_key:
50
+ keys.append(("agent_key", f"key:{agent_key}"))
51
+ return keys
52
+
53
+ async def check(
54
+ self,
55
+ *,
56
+ ip: str | None = None,
57
+ agent_key: str | None = None,
58
+ cost: float = 1.0,
59
+ policy: RateLimitPolicy | None = None,
60
+ ) -> RateLimitDecision:
61
+ """Admit or reject one request for the given principals (optionally under a tier's policy)."""
62
+ return await self.check_keys(self.keys_for(ip, agent_key, policy), cost=cost, policy=policy)
63
+
64
+ async def check_keys(
65
+ self, keys: Iterable[tuple[str, str]], cost: float = 1.0, policy: RateLimitPolicy | None = None
66
+ ) -> RateLimitDecision:
67
+ policy = policy or self.policy
68
+ keys = list(keys)
69
+ if not keys:
70
+ limit = policy.requests_per_minute
71
+ return RateLimitDecision(allowed=True, limit=limit, remaining=limit, retry_after=0.0)
72
+ return await self.store.rate_limit(
73
+ keys,
74
+ limit=policy.requests_per_minute,
75
+ window=self.window_seconds,
76
+ burst=policy.burst,
77
+ cost=cost,
78
+ now=self._clock(),
79
+ )
80
+
81
+ def reset(self) -> None:
82
+ if isinstance(self.store, MemoryStore):
83
+ self.store.meters.clear()
84
+
85
+ def tracked_keys(self) -> int:
86
+ """Number of principals tracked in process (in-memory store only)."""
87
+ return len(self.store.meters) if isinstance(self.store, MemoryStore) else 0
88
+
89
+
90
+ __all__ = ["RateLimitDecision", "RateLimiter", "SlidingWindowCounter", "TokenBucket"]
wap/server/router.py ADDED
@@ -0,0 +1,358 @@
1
+ """HTTP endpoints for WAP/1.0: ``/.well-known/wap.json``, ``/wap/v1/challenge`` and ``/wap/v1/interact``.
2
+
3
+ The interaction endpoint enforces the request pipeline defined in
4
+ ``docs/spec_rfc.md`` Section 7.2, in this order:
5
+
6
+ 1. per-IP rate limit (cheapest check first; configurable or disabled),
7
+ 2. protocol version negotiation,
8
+ 3. envelope validation,
9
+ 4. Ed25519 signature verification against the sender's declared key,
10
+ 5. freshness window and replay detection (shared through the state store),
11
+ 6. bearer authentication,
12
+ 7. admission hook, per-agent-key rate limit and proof-of-work (only now is a
13
+ challenge consumed),
14
+ 8. turn: session lock and ownership, idempotent-retry resolution, loop guards,
15
+ 9. dispatch to the capability or intent handler.
16
+
17
+ Nothing expensive (tools, databases, LLMs) runs before step 9.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import re
24
+ import time
25
+ from collections.abc import AsyncIterator
26
+ from typing import TYPE_CHECKING, Any
27
+
28
+ from fastapi import APIRouter, Request
29
+ from fastapi.responses import Response
30
+ from pydantic import ValidationError
31
+ from sse_starlette.sse import EventSourceResponse
32
+
33
+ from ..spec.crypto import canonical_json, verify_model
34
+ from ..spec.models import (
35
+ CHALLENGE_PATH,
36
+ ERROR_STATUS,
37
+ HEADER_SIGNATURE,
38
+ HEADER_VERSION,
39
+ INTERACT_PATH,
40
+ LEGACY_WELL_KNOWN_PATH,
41
+ WAP_VERSION,
42
+ WELL_KNOWN_PATH,
43
+ AgentMessage,
44
+ ErrorCode,
45
+ ErrorResponse,
46
+ Role,
47
+ StreamEventType,
48
+ )
49
+ from .admission import AdmissionRequest
50
+ from .app import Chunk, Turn, WAPProtocolError
51
+
52
+ if TYPE_CHECKING:
53
+ from .app import WAPServer
54
+
55
+ HEADER_IDEMPOTENT_REPLAY = "X-WAP-Idempotent-Replay"
56
+
57
+ MAX_BODY_BYTES = 256 * 1024
58
+ _TOKEN_SPLIT_RE = re.compile(r"(\s+)")
59
+
60
+
61
+ def client_ip(request: Request, trust_forwarded_for: bool) -> str | None:
62
+ if trust_forwarded_for:
63
+ forwarded = request.headers.get("x-forwarded-for")
64
+ if forwarded:
65
+ return forwarded.split(",")[0].strip()
66
+ return request.client.host if request.client else None
67
+
68
+
69
+ def signed_json_response(
70
+ server: WAPServer, payload: Any, status_code: int = 200, headers: dict[str, str] | None = None
71
+ ) -> Response:
72
+ """Serialise ``payload`` canonically and sign the exact body bytes (``X-WAP-Signature``)."""
73
+ body = canonical_json(payload)
74
+ all_headers = {
75
+ HEADER_VERSION: WAP_VERSION,
76
+ HEADER_SIGNATURE: server.signer.sign(body),
77
+ **(headers or {}),
78
+ }
79
+ return Response(content=body, status_code=status_code, media_type="application/json", headers=all_headers)
80
+
81
+
82
+ def error_response(server: WAPServer, error: WAPProtocolError, extra_headers: dict[str, str] | None = None) -> Response:
83
+ payload = ErrorResponse.build(
84
+ error.code, error.message, retry_after=error.retry_after, details=error.details
85
+ ).model_dump(mode="json", exclude_none=True)
86
+ headers = {**error.headers, **(extra_headers or {})}
87
+ if error.retry_after is not None and "Retry-After" not in headers:
88
+ headers["Retry-After"] = str(max(1, int(error.retry_after + 0.999)))
89
+ return signed_json_response(server, payload, ERROR_STATUS[error.code], headers)
90
+
91
+
92
+ def manifest_response(server: WAPServer) -> Response:
93
+ manifest = server.manifest()
94
+ max_age = 300
95
+ if manifest.expires_at is not None:
96
+ max_age = max(0, min(max_age, int(manifest.expires_at - time.time())))
97
+ return signed_json_response(
98
+ server,
99
+ manifest.model_dump(mode="json"),
100
+ headers={
101
+ "Cache-Control": f"public, max-age={max_age}",
102
+ "Access-Control-Allow-Origin": "*",
103
+ "ETag": f'"{manifest.signature[:32]}"',
104
+ },
105
+ )
106
+
107
+
108
+ def _split_tokens(text: str, words_per_token: int = 1) -> list[str]:
109
+ parts = [p for p in _TOKEN_SPLIT_RE.split(text) if p]
110
+ tokens: list[str] = []
111
+ buffer = ""
112
+ count = 0
113
+ for part in parts:
114
+ buffer += part
115
+ if not part.isspace():
116
+ count += 1
117
+ if count >= words_per_token:
118
+ tokens.append(buffer)
119
+ buffer, count = "", 0
120
+ if buffer:
121
+ tokens.append(buffer)
122
+ return tokens
123
+
124
+
125
+ async def _admit(server: WAPServer, request: Request) -> tuple[Turn, dict[str, str]]:
126
+ """Run pipeline steps 1-8 and return an opened turn plus response headers."""
127
+ ip = client_ip(request, server.trust_forwarded_for)
128
+ headers = await server.limit_ip(ip)
129
+
130
+ version = request.headers.get(HEADER_VERSION)
131
+ if version is not None and version.split(".")[0] != WAP_VERSION.split(".")[0]:
132
+ raise WAPProtocolError(
133
+ ErrorCode.UNSUPPORTED_VERSION,
134
+ f"unsupported WAP version {version!r}; this agent speaks {WAP_VERSION}",
135
+ details={"supported": [WAP_VERSION]},
136
+ )
137
+
138
+ body = await request.body()
139
+ if len(body) > MAX_BODY_BYTES:
140
+ raise WAPProtocolError(ErrorCode.INVALID_REQUEST, f"request body exceeds {MAX_BODY_BYTES} bytes")
141
+ try:
142
+ message = AgentMessage.model_validate_json(body)
143
+ except ValidationError as exc:
144
+ raise WAPProtocolError(
145
+ ErrorCode.INVALID_REQUEST,
146
+ "request body is not a valid AgentMessage",
147
+ details={"errors": exc.errors(include_url=False, include_context=False, include_input=False)},
148
+ ) from exc
149
+
150
+ if message.role is not Role.USER_AGENT:
151
+ raise WAPProtocolError(ErrorCode.INVALID_REQUEST, "requests must carry role 'user_agent'")
152
+ if message.public_key is None:
153
+ raise WAPProtocolError(ErrorCode.INVALID_SIGNATURE, "user_agent messages must declare public_key")
154
+ if not verify_model(message, message.public_key):
155
+ raise WAPProtocolError(ErrorCode.INVALID_SIGNATURE, "message signature does not verify against public_key")
156
+
157
+ await server.check_replay(message)
158
+ principal = await server.authenticate(request.headers.get("authorization"))
159
+ decision, key_headers = await server.admit(
160
+ AdmissionRequest(
161
+ client_ip=ip,
162
+ agent_key=message.public_key,
163
+ principal=principal,
164
+ capability_id=message.capability_id,
165
+ transport="wap",
166
+ headers=dict(request.headers),
167
+ ),
168
+ pow_seed=message.pow_seed,
169
+ pow_nonce=message.pow_nonce,
170
+ )
171
+ turn = server.open_turn(
172
+ message,
173
+ owner_key=message.public_key,
174
+ principal_key=f"key:{message.public_key}",
175
+ client_ip=ip,
176
+ agent_key=message.public_key,
177
+ principal=principal,
178
+ tier=decision.tier,
179
+ conversation_policy=decision.conversation_policy,
180
+ )
181
+ await turn.open()
182
+ return turn, {**headers, **key_headers}
183
+
184
+
185
+ def _encode(event: StreamEventType, payload: Any) -> dict[str, str]:
186
+ return {"event": event.value, "data": json.dumps(payload, separators=(",", ":"), ensure_ascii=False)}
187
+
188
+
189
+ def build_router(server: WAPServer) -> APIRouter:
190
+ router = APIRouter(tags=["WebAgent Protocol"])
191
+
192
+ @router.get(WELL_KNOWN_PATH, include_in_schema=True, summary="WAP discovery manifest (RFC 8615)")
193
+ @router.get(LEGACY_WELL_KNOWN_PATH, include_in_schema=False)
194
+ async def well_known_agent(request: Request) -> Response:
195
+ try:
196
+ headers = await server.limit_ip(client_ip(request, server.trust_forwarded_for))
197
+ except WAPProtocolError as exc:
198
+ return error_response(server, exc)
199
+ response = manifest_response(server)
200
+ response.headers.update(headers)
201
+ return response
202
+
203
+ @router.get(CHALLENGE_PATH, summary="Issue a proof-of-work challenge")
204
+ async def challenge(request: Request) -> Response:
205
+ try:
206
+ headers = await server.limit_ip(client_ip(request, server.trust_forwarded_for))
207
+ except WAPProtocolError as exc:
208
+ return error_response(server, exc)
209
+ if not server.require_pow and server.admission is None:
210
+ return error_response(
211
+ server, WAPProtocolError(ErrorCode.INVALID_REQUEST, "proof-of-work is not enabled on this agent")
212
+ )
213
+ issued = await server.issue_challenge()
214
+ return signed_json_response(
215
+ server, issued.model_dump(mode="json"), headers={"Cache-Control": "no-store", **headers}
216
+ )
217
+
218
+ @router.post(INTERACT_PATH, summary="Send a signed AgentMessage; JSON or SSE reply")
219
+ async def interact(request: Request) -> Response:
220
+ try:
221
+ turn, headers = await _admit(server, request)
222
+ except WAPProtocolError as exc:
223
+ if exc.code is not ErrorCode.RATE_LIMITED:
224
+ server.emit("request.rejected", code=exc.code.value)
225
+ return error_response(server, exc)
226
+
227
+ wants_stream = "text/event-stream" in request.headers.get("accept", "")
228
+
229
+ if turn.replay is not None:
230
+ await turn.close()
231
+ replay = turn.replay
232
+ headers = {**headers, HEADER_IDEMPOTENT_REPLAY: "true"}
233
+ if not wants_stream:
234
+ return signed_json_response(server, replay.model_dump(mode="json"), headers=headers)
235
+
236
+ async def replay_stream() -> AsyncIterator[dict[str, str]]:
237
+ yield _encode(
238
+ StreamEventType.META,
239
+ {
240
+ "session_id": replay.session_id,
241
+ "in_reply_to": replay.in_reply_to,
242
+ "capability_id": replay.capability_id,
243
+ "wap_version": WAP_VERSION,
244
+ "idempotent_replay": True,
245
+ },
246
+ )
247
+ if replay.structured_data:
248
+ yield _encode(StreamEventType.DATA, replay.structured_data)
249
+ for token in _split_tokens(replay.content):
250
+ yield _encode(StreamEventType.TOKEN, {"text": token})
251
+ yield _encode(StreamEventType.MESSAGE, replay.model_dump(mode="json"))
252
+
253
+ return EventSourceResponse(
254
+ replay_stream(), headers={HEADER_VERSION: WAP_VERSION, "Cache-Control": "no-store", **headers}
255
+ )
256
+
257
+ ctx = turn.ctx
258
+ assert ctx is not None
259
+ chunks = server.dispatch(ctx).__aiter__()
260
+
261
+ # Pull the first chunk before committing to a status code, so that
262
+ # validation / unknown-capability / auth errors surface as HTTP errors.
263
+ first: Chunk | None = None
264
+ exhausted = False
265
+ try:
266
+ first = await chunks.__anext__()
267
+ except StopAsyncIteration:
268
+ exhausted = True
269
+ except WAPProtocolError as exc:
270
+ await turn.fail(exc)
271
+ await turn.close()
272
+ return error_response(server, exc, headers)
273
+
274
+ if not wants_stream:
275
+ text: list[str] = []
276
+ data: dict[str, Any] = {}
277
+
278
+ def collect(chunk: Chunk) -> None:
279
+ if isinstance(chunk, str):
280
+ text.append(chunk)
281
+ else:
282
+ data.update(chunk)
283
+
284
+ try:
285
+ if not exhausted:
286
+ collect(first) # type: ignore[arg-type]
287
+ async for chunk in chunks:
288
+ collect(chunk)
289
+ reply = await turn.complete("".join(text), data or None)
290
+ except WAPProtocolError as exc:
291
+ await turn.fail(exc)
292
+ return error_response(server, exc, headers)
293
+ finally:
294
+ await turn.close()
295
+ return signed_json_response(server, reply.model_dump(mode="json"), headers=headers)
296
+
297
+ async def event_stream() -> AsyncIterator[dict[str, str]]:
298
+ text: list[str] = []
299
+ data: dict[str, Any] = {}
300
+
301
+ async def emit(chunk: Chunk) -> AsyncIterator[dict[str, str]]:
302
+ if isinstance(chunk, str):
303
+ text.append(chunk)
304
+ for token in _split_tokens(chunk):
305
+ yield _encode(StreamEventType.TOKEN, {"text": token})
306
+ else:
307
+ data.update(chunk)
308
+ yield _encode(StreamEventType.DATA, chunk)
309
+
310
+ try:
311
+ yield _encode(
312
+ StreamEventType.META,
313
+ {
314
+ "session_id": ctx.session_id,
315
+ "in_reply_to": ctx.message.message_id,
316
+ "capability_id": ctx.message.capability_id,
317
+ "wap_version": WAP_VERSION,
318
+ },
319
+ )
320
+ try:
321
+ if not exhausted:
322
+ async for event in emit(first): # type: ignore[arg-type]
323
+ yield event
324
+ async for chunk in chunks:
325
+ async for event in emit(chunk):
326
+ yield event
327
+ reply = await turn.complete("".join(text), data or None)
328
+ except WAPProtocolError as exc:
329
+ await turn.fail(exc)
330
+ yield _encode(
331
+ StreamEventType.ERROR,
332
+ ErrorResponse.build(exc.code, exc.message, details=exc.details).model_dump(
333
+ mode="json", exclude_none=True
334
+ ),
335
+ )
336
+ return
337
+ yield _encode(StreamEventType.MESSAGE, reply.model_dump(mode="json"))
338
+ finally:
339
+ # Also runs if the client disconnects mid-stream (generator closed/cancelled).
340
+ await turn.close()
341
+
342
+ return EventSourceResponse(
343
+ event_stream(),
344
+ headers={HEADER_VERSION: WAP_VERSION, "Cache-Control": "no-store", **headers},
345
+ ping=15,
346
+ )
347
+
348
+ return router
349
+
350
+
351
+ __all__ = [
352
+ "HEADER_IDEMPOTENT_REPLAY",
353
+ "build_router",
354
+ "client_ip",
355
+ "error_response",
356
+ "manifest_response",
357
+ "signed_json_response",
358
+ ]
wap/server/state.py ADDED
@@ -0,0 +1,117 @@
1
+ """Dialogue sessions persisted in a :class:`~wap.storage.StateStore`.
2
+
3
+ A session is a JSON document holding the conversation history, the
4
+ application's own ``state`` dict (what actions read and write through
5
+ ``ctx.state``), the owning agent key and the loop-guard history. It is loaded
6
+ at the start of a turn under a per-session lock and saved at the end, so
7
+ concurrent requests for the same session are serialised across every worker
8
+ sharing the store.
9
+
10
+ Values written to ``ctx.state`` must therefore be JSON-serialisable.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import time
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+ from ..spec.conversation import ConversationGuard, ConversationPolicy
21
+ from ..spec.models import AgentMessage, ErrorCode
22
+ from ..storage.base import StateStore
23
+
24
+
25
+ @dataclass
26
+ class SessionState:
27
+ session_id: str
28
+ owner_key: str | None
29
+ created_at: float = field(default_factory=time.time)
30
+ last_seen: float = field(default_factory=time.time)
31
+ history: list[AgentMessage] = field(default_factory=list)
32
+ state: dict[str, Any] = field(default_factory=dict)
33
+ guard: ConversationGuard | None = None
34
+
35
+ def to_dict(self) -> dict[str, Any]:
36
+ return {
37
+ "session_id": self.session_id,
38
+ "owner_key": self.owner_key,
39
+ "created_at": self.created_at,
40
+ "last_seen": self.last_seen,
41
+ "history": [m.model_dump(mode="json") for m in self.history],
42
+ "state": self.state,
43
+ "guard": self.guard.to_dict() if self.guard is not None else None,
44
+ }
45
+
46
+ @classmethod
47
+ def from_dict(cls, data: dict[str, Any], policy: ConversationPolicy | None) -> SessionState:
48
+ return cls(
49
+ session_id=data["session_id"],
50
+ owner_key=data.get("owner_key"),
51
+ created_at=float(data.get("created_at", time.time())),
52
+ last_seen=float(data.get("last_seen", time.time())),
53
+ history=[AgentMessage.model_validate(m) for m in data.get("history", [])],
54
+ state=dict(data.get("state") or {}),
55
+ guard=ConversationGuard.from_dict(policy, data.get("guard")) if policy is not None else None,
56
+ )
57
+
58
+
59
+ class SessionManager:
60
+ """Loads, saves and locks sessions in a shared store."""
61
+
62
+ def __init__(
63
+ self,
64
+ store: StateStore,
65
+ *,
66
+ ttl_seconds: float = 3600.0,
67
+ max_history: int = 100,
68
+ conversation_policy: ConversationPolicy | None = None,
69
+ lock_timeout: float = 120.0,
70
+ lock_wait: float = 10.0,
71
+ ) -> None:
72
+ self.store = store
73
+ self.ttl_seconds = ttl_seconds
74
+ self.max_history = max_history
75
+ self.conversation_policy = conversation_policy
76
+ self.lock_timeout = lock_timeout
77
+ self.lock_wait = lock_wait
78
+
79
+ @staticmethod
80
+ def _key(session_id: str) -> str:
81
+ return f"session:{session_id}"
82
+
83
+ def lock(self, session_id: str) -> Any:
84
+ return self.store.lock(self._key(session_id), timeout=self.lock_timeout, wait=self.lock_wait)
85
+
86
+ async def get(self, session_id: str) -> SessionState | None:
87
+ data = await self.store.get(self._key(session_id))
88
+ return None if data is None else SessionState.from_dict(data, self.conversation_policy)
89
+
90
+ async def load(self, session_id: str, owner_key: str | None) -> SessionState:
91
+ """Return the session, creating it if absent. Raises if another key owns it."""
92
+ from .app import WAPProtocolError
93
+
94
+ session = await self.get(session_id)
95
+ if session is None:
96
+ guard = ConversationGuard(self.conversation_policy) if self.conversation_policy is not None else None
97
+ return SessionState(session_id=session_id, owner_key=owner_key, guard=guard)
98
+ if session.owner_key != owner_key:
99
+ raise WAPProtocolError(ErrorCode.FORBIDDEN, "session belongs to a different agent key")
100
+ return session
101
+
102
+ async def save(self, session: SessionState) -> None:
103
+ session.last_seen = time.time()
104
+ if len(session.history) > self.max_history:
105
+ del session.history[: len(session.history) - self.max_history]
106
+ document = session.to_dict()
107
+ try:
108
+ json.dumps(document)
109
+ except (TypeError, ValueError) as exc:
110
+ raise TypeError(
111
+ f"session {session.session_id!r} state is not JSON-serialisable ({exc}); "
112
+ "store only JSON values in ctx.state"
113
+ ) from exc
114
+ await self.store.set(self._key(session.session_id), document, ttl=self.ttl_seconds)
115
+
116
+
117
+ __all__ = ["SessionManager", "SessionState"]
wap/spec/__init__.py ADDED
@@ -0,0 +1,75 @@
1
+ """WAP/1.0 specification layer: wire models, signatures and proof-of-work."""
2
+
3
+ from .conversation import ConversationGuard, ConversationLimitError, ConversationPolicy
4
+ from .crypto import (
5
+ KeyFormatError,
6
+ KeyPair,
7
+ Signer,
8
+ canonical_json,
9
+ fingerprint,
10
+ generate_keypair,
11
+ sign_model,
12
+ signing_payload,
13
+ verify_bytes,
14
+ verify_model,
15
+ )
16
+ from .models import (
17
+ CHALLENGE_PATH,
18
+ HEADER_AGENT_KEY,
19
+ HEADER_SIGNATURE,
20
+ HEADER_VERSION,
21
+ INTERACT_PATH,
22
+ WAP_VERSION,
23
+ WELL_KNOWN_PATH,
24
+ AgentManifest,
25
+ AgentMessage,
26
+ Capability,
27
+ Challenge,
28
+ ErrorCode,
29
+ ErrorDetail,
30
+ ErrorResponse,
31
+ RateLimitPolicy,
32
+ Role,
33
+ StreamEventType,
34
+ normalize_authority,
35
+ )
36
+ from .pow import PowEngine, PowError, check_solution, solve, solve_async
37
+
38
+ __all__ = [
39
+ "CHALLENGE_PATH",
40
+ "HEADER_AGENT_KEY",
41
+ "HEADER_SIGNATURE",
42
+ "HEADER_VERSION",
43
+ "INTERACT_PATH",
44
+ "WAP_VERSION",
45
+ "WELL_KNOWN_PATH",
46
+ "AgentManifest",
47
+ "AgentMessage",
48
+ "Capability",
49
+ "Challenge",
50
+ "ConversationGuard",
51
+ "ConversationLimitError",
52
+ "ConversationPolicy",
53
+ "ErrorCode",
54
+ "ErrorDetail",
55
+ "ErrorResponse",
56
+ "KeyFormatError",
57
+ "KeyPair",
58
+ "PowEngine",
59
+ "PowError",
60
+ "RateLimitPolicy",
61
+ "Role",
62
+ "Signer",
63
+ "StreamEventType",
64
+ "canonical_json",
65
+ "check_solution",
66
+ "fingerprint",
67
+ "generate_keypair",
68
+ "normalize_authority",
69
+ "sign_model",
70
+ "signing_payload",
71
+ "solve",
72
+ "solve_async",
73
+ "verify_bytes",
74
+ "verify_model",
75
+ ]