mintid-agent 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.
@@ -0,0 +1,89 @@
1
+ """mintid-agent — the MintID agent client (MINT-342).
2
+
3
+ An agent runtime's side of the agent channel, over the holder library
4
+ (``proof_core.holder``): ``AgentClient.issue_from_offer`` (the principal's
5
+ mint presentation redeems an agent offer), ``refresh`` (status witness +
6
+ bulk scope delta replay), ``present`` (fail closed, naming the output key),
7
+ ``pay`` (the bIP-0002/1 x402 identity loop, Web Bot Auth-signed) and
8
+ ``renew`` (MINT-19 replacement). Keys never leave the client: the
9
+ credential's private bytes go only to an injected ``CredentialStore``
10
+ (encrypt-at-rest is the integrator's), the signing key stays behind an
11
+ injected ``Signer``. See ``docs/sdk/12-agent-clients.md``.
12
+
13
+ The pairwise agent pseudonym (R-A10) is mandatory in every ``bip-0002/1``
14
+ policy since contract 1.4.0: ``pay`` / ``present`` need nothing for it —
15
+ the holder proves the pseudonym for the challenge's own verifier (and
16
+ refuses a challenge naming another). ``unexpired`` is mandatory too since
17
+ 1.5.0 (a declaration without it is ``unexpired_required``); an agent past
18
+ its lease cannot answer anywhere, and ``refresh`` reports the issuer's lease
19
+ sweep as ``CredentialRevoked`` from the next root on. A vendor session ends
20
+ with the status root it was decided under, so ``pay`` presents again after
21
+ each new root. Not in this version: sub-delegation
22
+ (``agent.delegate`` is gated)."""
23
+
24
+ from proof_core.holder import (
25
+ CredentialRevoked,
26
+ HolderError,
27
+ SelfRevocationReceipt,
28
+ SelfRevocationRefused,
29
+ WitnessState,
30
+ )
31
+
32
+ from mintid_agent.client import AgentClient, Payer, PaymentResult
33
+ from mintid_agent.errors import (
34
+ AgentClientError,
35
+ ChallengeError,
36
+ IdentityRefused,
37
+ PolicyUnsatisfiable,
38
+ )
39
+ from mintid_agent.issuance import redeem_agent_offer
40
+ from mintid_agent.store import CredentialStore, MemoryStore
41
+ from mintid_agent.web_bot_auth import (
42
+ Ed25519Signer,
43
+ Signer,
44
+ directory_document,
45
+ okp_thumbprint,
46
+ sign_headers,
47
+ )
48
+ from mintid_agent.x402 import (
49
+ PRESENTATION_HEADER,
50
+ SESSION_HEADER,
51
+ Http,
52
+ HttpResponse,
53
+ IdentityDeclaration,
54
+ encode_presentation_header,
55
+ urllib_http,
56
+ )
57
+
58
+ __version__ = "0.1.0"
59
+
60
+ __all__ = [
61
+ "PRESENTATION_HEADER",
62
+ "SESSION_HEADER",
63
+ "AgentClient",
64
+ "AgentClientError",
65
+ "ChallengeError",
66
+ "CredentialRevoked",
67
+ "CredentialStore",
68
+ "Ed25519Signer",
69
+ "HolderError",
70
+ "Http",
71
+ "HttpResponse",
72
+ "IdentityDeclaration",
73
+ "IdentityRefused",
74
+ "MemoryStore",
75
+ "Payer",
76
+ "PaymentResult",
77
+ "PolicyUnsatisfiable",
78
+ "SelfRevocationReceipt",
79
+ "SelfRevocationRefused",
80
+ "Signer",
81
+ "WitnessState",
82
+ "__version__",
83
+ "directory_document",
84
+ "encode_presentation_header",
85
+ "okp_thumbprint",
86
+ "redeem_agent_offer",
87
+ "sign_headers",
88
+ "urllib_http",
89
+ ]
mintid_agent/client.py ADDED
@@ -0,0 +1,504 @@
1
+ """``AgentClient`` — one agent credential in its runtime's custody.
2
+
3
+ - **Issuance** (``issue_from_offer``): the principal's mint presentation
4
+ redeems an agent offer; the scope witnesses are adopted with their delta
5
+ cursor (``issuance``). ``renew`` redeems a renewal offer (MINT-19:
6
+ replacement — the old credential is revoked on issuance).
7
+ - **Freshness** (``refresh``): the status witness, then the issuer's public
8
+ scope delta log replayed in bulk from the cursor (``?since=<cursor>``,
9
+ never a per-value query — agent-presentation-v1 §6). A withdrawn
10
+ delegation then refuses at the holder: fail closed, no proof emitted.
11
+ - **Presenting** (``present``): the holder library answers a §10.5
12
+ challenge or refuses, naming the output key (``PolicyUnsatisfiable``).
13
+ - **Paying** (``pay``): the x402 loop of bIP-0002/1 — see ``pay``.
14
+ - **Kill switch** (``self_revoke``): R11 self-revocation of this agent
15
+ credential (self-revocation-v1 §5.5), submitted through a privacy relay
16
+ that pays for the transaction (MINT-348); the receipt is confirmed
17
+ against the chain on ``chain_rpc`` (MINT-372) through the issuer-scoped
18
+ ``RecordedNullifiers`` read, which names no nullifier (MINT-377).
19
+
20
+ Custody: the holder's private bytes stay in the holder library and reach
21
+ only the injected ``CredentialStore`` (``store``); the Web Bot Auth key
22
+ stays behind the injected ``Signer``. Nothing here logs or returns either.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import os
29
+ import time
30
+ import urllib.parse
31
+ from collections.abc import Callable, Mapping
32
+ from dataclasses import dataclass, field
33
+
34
+ from proof_core.chain import NullifierReader
35
+ from proof_core.holder import (
36
+ Holder,
37
+ HolderError,
38
+ SelfRevocationReceipt,
39
+ Transport,
40
+ WitnessState,
41
+ submit_self_revocation,
42
+ )
43
+
44
+ from mintid_agent import issuance
45
+ from mintid_agent.errors import ChallengeError, IdentityRefused, PolicyUnsatisfiable
46
+ from mintid_agent.store import CredentialStore, CustodyRecord
47
+ from mintid_agent.web_bot_auth import Signer, sign_headers
48
+ from mintid_agent.x402 import (
49
+ PAYMENT_HEADER,
50
+ PRESENTATION_HEADER,
51
+ SESSION_HEADER,
52
+ Http,
53
+ HttpResponse,
54
+ IdentityDeclaration,
55
+ encode_presentation_header,
56
+ origin_of,
57
+ reason_of,
58
+ urllib_http,
59
+ )
60
+
61
+ STALE_ROOT = "status_root_stale"
62
+ # Reasons a vendor's identity leg gives that call for a (new) presentation:
63
+ # the session states, and the one rejection a buyer agent retries —
64
+ # ``status_root_stale``, after a refresh. Every other rejection is terminal
65
+ # for the attempt (bIP-0002 §10, v1 buyer agent conformance).
66
+ REPRESENT = frozenset(
67
+ {
68
+ "identity_required",
69
+ "session_expired",
70
+ "session_key_mismatch",
71
+ STALE_ROOT,
72
+ }
73
+ )
74
+ MAX_PRESENTATIONS = 2 # per payment: the first, and one after a retryable answer
75
+
76
+ Payer = Callable[[object], str]
77
+ """``payer(payment_required_body) -> X-PAYMENT value`` — the x402 payment
78
+ scheme is the integrator's (wallet, facilitator client); the client calls it
79
+ once per payment, and only after the identity leg can be answered."""
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class PaymentResult:
84
+ status: int
85
+ headers: Mapping[str, str]
86
+ body: object
87
+ presented: bool # a fresh presentation was sent for this payment
88
+ session_reused: bool # the vendor accepted an existing session token
89
+ paid: bool # an X-PAYMENT was sent
90
+
91
+
92
+ @dataclass
93
+ class AgentClient:
94
+ holder: Holder
95
+ credential_id: str
96
+ expires_at: int
97
+ scope_endpoint: str
98
+ transport: Transport
99
+ scope_cursor: int = 0
100
+ store: CredentialStore | None = None
101
+ http: Http | None = None
102
+ signer: Signer | None = None
103
+ signature_agent: str | None = None
104
+ clock: Callable[[], float] = time.time
105
+ sessions: dict[str, str] = field(default_factory=dict) # authority → token
106
+ last_refresh: float | None = None
107
+ presentations: int = 0
108
+ # The node the kill switch confirms its receipt on (MINT-372): a CometBFT
109
+ # RPC URL or a ``NullifierReader``; ``None`` → ``$MINTID_CHAIN_RPC``, then
110
+ # ``$MINTID_RPC`` (https://rpc.anchor.mintid.net on the anchor).
111
+ chain_rpc: str | NullifierReader | None = None
112
+ # The bounded-agent bundle it was minted from (MINT-403): "" for a raw
113
+ # scope. Informative for the runtime; the proof is what a verifier sees.
114
+ bundle_id: str = ""
115
+
116
+ # ── construction ────────────────────────────────────────────────
117
+
118
+ @classmethod
119
+ def issue_from_offer(
120
+ cls,
121
+ offer: dict,
122
+ principal: Holder,
123
+ *,
124
+ transport: Transport,
125
+ store: CredentialStore | None = None,
126
+ **options,
127
+ ) -> AgentClient:
128
+ """Redeem an agent offer with the principal's holder. ``HolderError``
129
+ on a terminal issuer refusal, ``ValueError`` when the offer is not an
130
+ agent offer or the principal cannot answer the mint challenge."""
131
+ issued = issuance.redeem_agent_offer(offer, principal, transport=transport)
132
+ client = cls(
133
+ holder=issued.holder,
134
+ credential_id=issued.credential_id,
135
+ expires_at=issued.expires_at,
136
+ scope_endpoint=issued.scope_endpoint,
137
+ transport=transport,
138
+ scope_cursor=issued.scope_cursor,
139
+ store=store,
140
+ bundle_id=issued.bundle_id,
141
+ **options,
142
+ )
143
+ client.persist()
144
+ return client
145
+
146
+ @classmethod
147
+ def load(
148
+ cls,
149
+ credential_id: str,
150
+ store: CredentialStore,
151
+ *,
152
+ transport: Transport,
153
+ **options,
154
+ ) -> AgentClient:
155
+ """Resume a credential from the store (``KeyError`` if absent)."""
156
+ raw = store.load(credential_id)
157
+ if raw is None:
158
+ raise KeyError(credential_id)
159
+ record = CustodyRecord.from_bytes(raw)
160
+ return cls(
161
+ holder=Holder.from_storage(record.storage, transport=transport),
162
+ credential_id=record.credential_id,
163
+ expires_at=record.expires_at,
164
+ scope_endpoint=record.scope_endpoint,
165
+ transport=transport,
166
+ scope_cursor=record.scope_cursor,
167
+ store=store,
168
+ bundle_id=record.bundle_id,
169
+ **options,
170
+ )
171
+
172
+ def renew(self, offer: dict, principal: Holder) -> AgentClient:
173
+ """Redeem a renewal offer (the operator's ``…/renew``): a new client
174
+ for the replacement credential with this one's runtime settings.
175
+ The issuer revokes this credential on issuance, so its record leaves
176
+ the store."""
177
+ new = AgentClient.issue_from_offer(
178
+ offer,
179
+ principal,
180
+ transport=self.transport,
181
+ store=self.store,
182
+ http=self.http,
183
+ signer=self.signer,
184
+ signature_agent=self.signature_agent,
185
+ clock=self.clock,
186
+ chain_rpc=self.chain_rpc,
187
+ )
188
+ if self.store is not None:
189
+ self.store.delete(self.credential_id)
190
+ return new
191
+
192
+ def persist(self) -> None:
193
+ if self.store is None:
194
+ return
195
+ self.store.save(
196
+ self.credential_id,
197
+ CustodyRecord(
198
+ credential_id=self.credential_id,
199
+ expires_at=self.expires_at,
200
+ scope_endpoint=self.scope_endpoint,
201
+ scope_cursor=self.scope_cursor,
202
+ storage=self.holder.storage_bytes(),
203
+ bundle_id=self.bundle_id,
204
+ ).to_bytes(),
205
+ )
206
+
207
+ # ── freshness ───────────────────────────────────────────────────
208
+
209
+ def refresh_scope(self) -> int:
210
+ """Replay the issuer's public delta log after the cursor (bulk);
211
+ returns the last delta sequence number the log reports."""
212
+ parts = urllib.parse.urlsplit(self.scope_endpoint)
213
+ url = urllib.parse.urlunsplit(
214
+ parts._replace(query=urllib.parse.urlencode({"since": self.scope_cursor}))
215
+ )
216
+ status, body = self.transport("GET", url)
217
+ if status != 200:
218
+ raise HolderError("scope_deltas", status, body)
219
+ last = int(body["last_seq"])
220
+ if last > self.scope_cursor:
221
+ self.holder.refresh_scope(bytes.fromhex(body["log"]))
222
+ self.scope_cursor = last
223
+ return last
224
+
225
+ def refresh(self) -> WitnessState:
226
+ """The status witness (``CredentialRevoked`` once the agent is out of
227
+ the accumulator), then — once a finalized root anchors it — the
228
+ scope delta log. Persists the result."""
229
+ state = self.holder.refresh_witness()
230
+ if state.usable:
231
+ self.refresh_scope()
232
+ self.last_refresh = self.clock()
233
+ self.persist()
234
+ return state
235
+
236
+ # ── presenting ──────────────────────────────────────────────────
237
+
238
+ def present(self, challenge: dict) -> dict:
239
+ """The envelope for a verifier's §10.5 challenge (``{request,
240
+ signature_hex}`` or the bare request). ``PolicyUnsatisfiable``
241
+ (a ``ValueError``) names the output key when the credential cannot
242
+ satisfy the policy — nothing leaves the holder then. Call
243
+ ``refresh`` first (``pay`` does): the holder proves over the witness
244
+ material it holds, and a proof over superseded scope anchors is a
245
+ verifier's ``proof_invalid``, not a holder refusal."""
246
+ try:
247
+ envelope = self.holder.present(challenge)
248
+ except ValueError as exc:
249
+ raise PolicyUnsatisfiable.from_holder(exc) from exc
250
+ self.presentations += 1
251
+ return envelope
252
+
253
+ def relay_disclosure(
254
+ self,
255
+ issuer_url: str,
256
+ message: dict,
257
+ prove: Callable[[dict], object] | None = None,
258
+ ) -> dict:
259
+ """MINT-406: relay a relying party's ``DisclosureOffer`` or
260
+ ``IdentificationRequest`` to this agent's issuer with the relay
261
+ proof over the issuer's challenge. The agent grants nothing and
262
+ receives no field: the answer is ``{state, reason}``
263
+ (``mintid_agent.disclosure``).
264
+
265
+ Without ``prove`` the holder proves (MINT-409: the relay-to-issuer
266
+ presentation, ``Holder.present_for_relay``) after a witness
267
+ ``refresh`` — the proof is made against the definition the holder
268
+ last refreshed to, which is the one the issuer accepts."""
269
+ from mintid_agent.disclosure import holder_prover, relay_disclosure
270
+
271
+ if prove is None:
272
+ self.refresh()
273
+ prove = holder_prover(self.holder)
274
+ return relay_disclosure(issuer_url, message, prove, http=self.http)
275
+
276
+ def info(self):
277
+ return self.holder.info()
278
+
279
+ # ── kill switch (R11) ───────────────────────────────────────────
280
+
281
+ def self_revoke(
282
+ self,
283
+ chain_id: str,
284
+ anchoring: dict | None = None,
285
+ relay_url: str | None = None,
286
+ *,
287
+ status_root: bytes | None = None,
288
+ relay_transport: Transport | None = None,
289
+ chain: str | NullifierReader | None = None,
290
+ **confirm,
291
+ ) -> dict | SelfRevocationReceipt:
292
+ """The agent's own kill switch (R-A6 per-agent self-revoke, R11;
293
+ self-revocation-v1 §5.2, §5.5).
294
+
295
+ The holder builds and checks the SRP-1 message and from then on
296
+ refuses to present; the custody record is persisted *before* any
297
+ submission, so the refusal survives a crash. ``anchoring`` is the
298
+ issuer metadata's ``anchoring`` object of a root in the chain's ring
299
+ (its ``agent`` member is taken when present); omitted, the material
300
+ the last ``refresh`` cached is used (the issuer serves it with the
301
+ witness, MINT-347). ``status_root``, when read from the chain, must
302
+ be that root.
303
+
304
+ Without ``relay_url`` the message is returned (``{message,
305
+ nullifier, ciphertext}``) for any relay to submit — see
306
+ ``submit_self_revocation``. With it, the message goes straight to
307
+ that privacy relay (MINT-348) and the receipt is returned, confirmed
308
+ on the chain (MINT-372; see ``submit_self_revocation``)."""
309
+ material = anchoring.get("agent", anchoring) if anchoring is not None else None
310
+ revocation = self.holder.self_revoke(chain_id, material, status_root)
311
+ self.persist()
312
+ if relay_url is None:
313
+ return revocation
314
+ return self.submit_self_revocation(
315
+ relay_url,
316
+ revocation,
317
+ relay_transport=relay_transport,
318
+ chain=chain,
319
+ **confirm,
320
+ )
321
+
322
+ def submit_self_revocation(
323
+ self,
324
+ relay_url: str,
325
+ revocation: dict,
326
+ *,
327
+ relay_transport: Transport | None = None,
328
+ chain: str | NullifierReader | None = None,
329
+ **confirm,
330
+ ) -> SelfRevocationReceipt:
331
+ """Hand a ``self_revoke`` result to a privacy relay (MINT-348, §5.3)
332
+ over ``relay_transport`` (default: a fresh stdlib transport, never
333
+ ``self.transport``, which talks to the issuer), then confirm it on
334
+ the chain (MINT-372): the receipt is ``pending``/``folded`` only when
335
+ the node (``chain``, else ``chain_rpc``, else ``$MINTID_CHAIN_RPC`` /
336
+ ``$MINTID_RPC``) shows the nullifier — whatever the relay answered,
337
+ ``nullifier_recorded`` included. Otherwise it is ``submitted`` with
338
+ ``unconfirmed_reason``: check again, or resubmit through another
339
+ relay (the message is idempotent). A refusal raises
340
+ ``SelfRevocationRefused``; the holder keeps refusing to present
341
+ either way. ``confirm``: ``chain_post``, ``confirm_attempts``,
342
+ ``confirm_interval``, ``sleep``."""
343
+ return submit_self_revocation(
344
+ relay_url,
345
+ revocation,
346
+ transport=relay_transport,
347
+ chain=self.node(chain),
348
+ **confirm,
349
+ )
350
+
351
+ def node(
352
+ self, chain: str | NullifierReader | None = None
353
+ ) -> str | NullifierReader | None:
354
+ """The node the kill switch reads: ``chain``, else ``chain_rpc``,
355
+ else ``$MINTID_CHAIN_RPC``, else ``$MINTID_RPC`` (``None``: no chain
356
+ endpoint — the receipt stays ``submitted``)."""
357
+ for candidate in (
358
+ chain,
359
+ self.chain_rpc,
360
+ os.environ.get("MINTID_CHAIN_RPC"),
361
+ os.environ.get("MINTID_RPC"),
362
+ ):
363
+ if candidate:
364
+ return candidate
365
+ return None
366
+
367
+ # ── x402 ────────────────────────────────────────────────────────
368
+
369
+ def _headers(self, authority: str) -> dict[str, str]:
370
+ if self.signer is None:
371
+ return {}
372
+ if not self.signature_agent:
373
+ raise ValueError("a Web Bot Auth signer needs its signature_agent URL")
374
+ return sign_headers(
375
+ self.signer,
376
+ authority=authority,
377
+ signature_agent=self.signature_agent,
378
+ now=int(self.clock()),
379
+ )
380
+
381
+ def _send(self, method, url, authority, extra, body) -> HttpResponse:
382
+ http = self.http or urllib_http()
383
+ headers = {**self._headers(authority), **extra}
384
+ return http(method, url, headers=headers, body=body)
385
+
386
+ def _identity_header(
387
+ self, declaration: IdentityDeclaration, resource_origin: str
388
+ ) -> str:
389
+ """Refresh → challenge → local satisfiability (the holder) → header
390
+ value. The refresh comes first so the 10-second window is the
391
+ proof's alone (bIP-0002 §10: refresh before presenting)."""
392
+ self.refresh()
393
+ endpoint_origin, endpoint_authority = origin_of(declaration.challenge_endpoint)
394
+ resp = self._send(
395
+ "POST",
396
+ declaration.challenge_endpoint,
397
+ endpoint_authority,
398
+ {"Content-Type": "application/json"},
399
+ b"{}",
400
+ )
401
+ challenge = resp.body
402
+ if resp.status not in (200, 201) or not isinstance(challenge, dict):
403
+ raise ChallengeError("challenge_failed", f"HTTP {resp.status}")
404
+ request = challenge.get("request")
405
+ if not isinstance(request, dict):
406
+ raise ChallengeError("challenge_failed", "no request object")
407
+ # Present only to the party the payment is for: the resource's
408
+ # origin (a self-verifying seller) or the declared endpoint's (the
409
+ # verifying party that fronts it).
410
+ if request.get("audience") not in (resource_origin, endpoint_origin):
411
+ raise ChallengeError("audience_mismatch")
412
+ envelope = self.present(challenge)
413
+ return encode_presentation_header(request, envelope, declaration.profile)
414
+
415
+ def pay(
416
+ self,
417
+ url: str,
418
+ *,
419
+ payer: Payer,
420
+ method: str = "GET",
421
+ body: dict | bytes | None = None,
422
+ headers: Mapping[str, str] | None = None,
423
+ ) -> PaymentResult:
424
+ """One paid request under bIP-0002/1:
425
+
426
+ request → ``402`` with ``extensions.mintid_identity`` → (no session
427
+ for this authority) ``POST challenge_endpoint`` → the holder proves
428
+ the declared policy or refuses (``PolicyUnsatisfiable``: nothing is
429
+ presented **and nothing is paid**) → ``payer`` builds ``X-PAYMENT``
430
+ → retry with ``X-PAYMENT`` + ``X-MINTID-PRESENTATION`` → on success
431
+ keep ``X-MINTID-SESSION`` for the authority, and send it (instead of
432
+ a presentation) on the next payment. The witnesses are refreshed
433
+ before every presentation. A session answer (``session_expired``,
434
+ ``identity_required``, ``session_key_mismatch``) or
435
+ ``status_root_stale`` (the root moved under the proof) gets one fresh
436
+ presentation; any other identity reason raises ``IdentityRefused``
437
+ (terminal for the attempt, bIP-0002 §10). Every request is Web Bot
438
+ Auth-signed when a signer is configured."""
439
+ origin, authority = origin_of(url)
440
+ raw = json.dumps(body).encode() if isinstance(body, dict) else body
441
+ base = dict(headers or {})
442
+ if isinstance(body, dict):
443
+ base.setdefault("Content-Type", "application/json")
444
+
445
+ resp = self._send(method, url, authority, base, raw)
446
+ if resp.status != 402:
447
+ return PaymentResult(
448
+ resp.status, resp.headers, resp.body, False, False, False
449
+ )
450
+ requirements = resp.body
451
+ declaration = IdentityDeclaration.from_payment_required(requirements)
452
+
453
+ payment: str | None = None
454
+ presented = False
455
+ use_session = declaration is not None and authority in self.sessions
456
+ fresh_presentations = 0
457
+ while True:
458
+ extra = dict(base)
459
+ reused = False
460
+ if declaration is not None:
461
+ if use_session:
462
+ extra[SESSION_HEADER] = self.sessions[authority]
463
+ reused = True
464
+ else:
465
+ extra[PRESENTATION_HEADER] = self._identity_header(
466
+ declaration, origin
467
+ )
468
+ presented = True
469
+ fresh_presentations += 1
470
+ if payment is None:
471
+ payment = payer(requirements)
472
+ extra[PAYMENT_HEADER] = payment
473
+ resp = self._send(method, url, authority, extra, raw)
474
+ if resp.status != 402 or declaration is None:
475
+ break
476
+ reason = reason_of(resp.body)
477
+ if reason is None:
478
+ break # the payment leg refused: the caller reads the 402
479
+ if reason not in REPRESENT or fresh_presentations >= MAX_PRESENTATIONS:
480
+ raise IdentityRefused(reason, resp.status, resp.body)
481
+ self.sessions.pop(authority, None)
482
+ use_session = False
483
+ if 200 <= resp.status < 300 and declaration is not None:
484
+ token = resp.header(SESSION_HEADER)
485
+ if token:
486
+ self.sessions[authority] = token
487
+ return PaymentResult(
488
+ resp.status,
489
+ resp.headers,
490
+ resp.body,
491
+ presented,
492
+ reused and 200 <= resp.status < 300,
493
+ True,
494
+ )
495
+
496
+
497
+ __all__ = [
498
+ "MAX_PRESENTATIONS",
499
+ "REPRESENT",
500
+ "STALE_ROOT",
501
+ "AgentClient",
502
+ "Payer",
503
+ "PaymentResult",
504
+ ]