agentaddress 0.9.1__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.
- aap/__init__.py +327 -0
- aap/address.py +88 -0
- aap/client.py +455 -0
- aap/conversations.py +533 -0
- aap/did_web.py +203 -0
- aap/discovery.py +356 -0
- aap/encryption.py +223 -0
- aap/envelope.py +169 -0
- aap/envelope_policy.py +133 -0
- aap/group_flow.py +128 -0
- aap/host_policy.py +58 -0
- aap/identity.py +157 -0
- aap/inbound.py +205 -0
- aap/jcs.py +17 -0
- aap/keys.py +74 -0
- aap/messages.py +154 -0
- aap/payloads.py +771 -0
- aap/pending_responses.py +55 -0
- aap/relationships.py +514 -0
- aap/service_followups.py +367 -0
- aap/services.py +456 -0
- aap/storage.py +30 -0
- aap/stores/__init__.py +1 -0
- aap/stores/attestations.py +231 -0
- aap/stores/consent.py +73 -0
- aap/stores/identity_bindings.py +94 -0
- aap/stores/outbound_contacts.py +91 -0
- aap/stores/pending_introductions.py +94 -0
- aap/stores/pending_proposals.py +159 -0
- aap/stores/service_request_groups.py +54 -0
- aap/stores/service_requests.py +183 -0
- aap/stores/verification_flow.py +118 -0
- aap/transport.py +49 -0
- aap/trusted_verifiers.py +88 -0
- aap/verifier_client.py +363 -0
- aap/verifiers.py +372 -0
- aap/version.py +1 -0
- agentaddress-0.9.1.dist-info/METADATA +498 -0
- agentaddress-0.9.1.dist-info/RECORD +41 -0
- agentaddress-0.9.1.dist-info/WHEEL +4 -0
- agentaddress-0.9.1.dist-info/licenses/LICENSE +203 -0
aap/__init__.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""aap — reference Python implementation of the Agent Address Protocol."""
|
|
2
|
+
|
|
3
|
+
from aap.address import Address
|
|
4
|
+
from aap.envelope import Envelope, EnvelopeError
|
|
5
|
+
from aap.encryption import (
|
|
6
|
+
ENCRYPTED_ENVELOPE_TYPE,
|
|
7
|
+
HPKE_ALGORITHM,
|
|
8
|
+
EncryptedEnvelope,
|
|
9
|
+
EncryptionError,
|
|
10
|
+
decrypt_envelope,
|
|
11
|
+
derive_encryption_keypair,
|
|
12
|
+
encrypt_envelope,
|
|
13
|
+
encryption_key_id,
|
|
14
|
+
encryption_public_from_private,
|
|
15
|
+
generate_encryption_keypair,
|
|
16
|
+
)
|
|
17
|
+
from aap.envelope_policy import (
|
|
18
|
+
DEFAULT_ENVELOPE_MAX_AGE_SECONDS,
|
|
19
|
+
DEFAULT_FUTURE_SKEW_SECONDS,
|
|
20
|
+
EnvelopePolicyError,
|
|
21
|
+
EnvelopeReplayCache,
|
|
22
|
+
envelope_replay_key,
|
|
23
|
+
parse_rfc3339,
|
|
24
|
+
validate_envelope_iat,
|
|
25
|
+
verify_envelope,
|
|
26
|
+
)
|
|
27
|
+
from aap.keys import (
|
|
28
|
+
decode_b64url,
|
|
29
|
+
encode_b64url,
|
|
30
|
+
generate_keypair,
|
|
31
|
+
seed_to_keypair,
|
|
32
|
+
sign,
|
|
33
|
+
verify,
|
|
34
|
+
)
|
|
35
|
+
from aap.payloads import (
|
|
36
|
+
AgentCard,
|
|
37
|
+
DiscoveryIntroductionRequest,
|
|
38
|
+
DiscoveryIntroductionResponse,
|
|
39
|
+
DiscoveryQueryResponse,
|
|
40
|
+
GroupComplete,
|
|
41
|
+
GroupInvitation,
|
|
42
|
+
GroupLeave,
|
|
43
|
+
GroupMembershipUpdate,
|
|
44
|
+
RelationshipAccept,
|
|
45
|
+
RelationshipDecline,
|
|
46
|
+
RelationshipProposal,
|
|
47
|
+
RelationshipRevoke,
|
|
48
|
+
ServiceFollowup,
|
|
49
|
+
ServiceFollowupGrant,
|
|
50
|
+
ServiceRequest,
|
|
51
|
+
ServiceResponse,
|
|
52
|
+
ServiceResponseStatus,
|
|
53
|
+
VerificationAttestation,
|
|
54
|
+
VerifyConfirmResponse,
|
|
55
|
+
VerifyStartResponse,
|
|
56
|
+
VerifiedIdentity,
|
|
57
|
+
)
|
|
58
|
+
from aap.group_flow import (
|
|
59
|
+
build_group_complete_envelope,
|
|
60
|
+
build_group_invitation_envelope,
|
|
61
|
+
build_group_leave_envelope,
|
|
62
|
+
build_group_membership_update_envelope,
|
|
63
|
+
)
|
|
64
|
+
from aap.messages import (
|
|
65
|
+
CHAT_PAYLOAD_TYPE,
|
|
66
|
+
ROUTING_ENVELOPE_TYPE,
|
|
67
|
+
UnsupportedPayloadType,
|
|
68
|
+
build_chat_envelope,
|
|
69
|
+
unwrap_chat_envelope,
|
|
70
|
+
wrap_routing_envelope,
|
|
71
|
+
)
|
|
72
|
+
from aap.host_policy import (
|
|
73
|
+
DEFAULT_LIFETIME_DAYS,
|
|
74
|
+
HIGH_RISK_LIFETIME_DAYS,
|
|
75
|
+
WILDCARD,
|
|
76
|
+
is_high_risk_scope,
|
|
77
|
+
should_auto_renew,
|
|
78
|
+
token_lifetime_days,
|
|
79
|
+
)
|
|
80
|
+
from aap.inbound import (
|
|
81
|
+
InboundPolicyError,
|
|
82
|
+
ValidatedChat,
|
|
83
|
+
ValidatedEnvelope,
|
|
84
|
+
validate_inbound_chat,
|
|
85
|
+
validate_inbound_envelope,
|
|
86
|
+
)
|
|
87
|
+
from aap.trusted_verifiers import VerifierTrustListEntry, parse_trusted_verifiers
|
|
88
|
+
from aap.transport import InsecureTransportError, require_secure_url
|
|
89
|
+
from aap.client import AAPClient, AAPClientError, AgentCardKeyChanged, KeyChangeRejected
|
|
90
|
+
from aap.did_web import (
|
|
91
|
+
DIDWebError,
|
|
92
|
+
KeyPinChanged,
|
|
93
|
+
KeyPins,
|
|
94
|
+
did_web_document_url,
|
|
95
|
+
did_web_domain,
|
|
96
|
+
resolve_did_web_key,
|
|
97
|
+
)
|
|
98
|
+
from aap.identity import IdentityFile, load_or_generate
|
|
99
|
+
from aap.verifier_client import (
|
|
100
|
+
VerifierClientError,
|
|
101
|
+
VerifyStartResult,
|
|
102
|
+
confirm_email_verification,
|
|
103
|
+
confirm_sms_verification,
|
|
104
|
+
start_email_verification,
|
|
105
|
+
start_sms_verification,
|
|
106
|
+
)
|
|
107
|
+
from aap.verifiers import (
|
|
108
|
+
DEFAULT_TRUSTED_VERIFIERS_URL,
|
|
109
|
+
TRUSTED_VERIFIERS_ISSUER,
|
|
110
|
+
TRUSTED_VERIFIERS_PAYLOAD_TYPE,
|
|
111
|
+
TrustListCache,
|
|
112
|
+
VerifierPubkeyCache,
|
|
113
|
+
trusted_verifiers_supporting,
|
|
114
|
+
verifier_relay_address,
|
|
115
|
+
)
|
|
116
|
+
from aap.relationships import (
|
|
117
|
+
RelationshipRecord,
|
|
118
|
+
RelationshipRevocationRecord,
|
|
119
|
+
RelationshipStore,
|
|
120
|
+
VALID_RELATIONSHIP_TYPES,
|
|
121
|
+
build_relationship_accept_envelope,
|
|
122
|
+
build_relationship_decline_envelope,
|
|
123
|
+
build_relationship_proposal_envelope,
|
|
124
|
+
build_relationship_revoke_envelope,
|
|
125
|
+
)
|
|
126
|
+
from aap.services import (
|
|
127
|
+
SERVICE_CATALOG_PAYLOAD_TYPE,
|
|
128
|
+
ServiceCatalog,
|
|
129
|
+
ServiceCatalogCache,
|
|
130
|
+
ServiceCatalogPayload,
|
|
131
|
+
ServiceDefinition,
|
|
132
|
+
ValidationFailure,
|
|
133
|
+
build_service_catalog_envelope,
|
|
134
|
+
build_service_request_envelope,
|
|
135
|
+
build_service_response_envelope,
|
|
136
|
+
validate_service_payload,
|
|
137
|
+
)
|
|
138
|
+
from aap.service_followups import (
|
|
139
|
+
FollowupGrantStore,
|
|
140
|
+
StoredFollowupGrant,
|
|
141
|
+
build_followup_envelope,
|
|
142
|
+
build_followup_grant_envelope,
|
|
143
|
+
parse_iso_duration,
|
|
144
|
+
)
|
|
145
|
+
from aap.conversations import (
|
|
146
|
+
Conversation,
|
|
147
|
+
ConversationEventRecord,
|
|
148
|
+
ConversationPolicyError,
|
|
149
|
+
ConversationStore,
|
|
150
|
+
broadcast_to_conversation,
|
|
151
|
+
)
|
|
152
|
+
from aap.pending_responses import PendingResponses
|
|
153
|
+
from aap.stores.attestations import AttestationStore, StoredAttestation
|
|
154
|
+
from aap.stores.consent import PendingConsent
|
|
155
|
+
from aap.stores.identity_bindings import IdentityBinding, IdentityBindingStore
|
|
156
|
+
from aap.stores.outbound_contacts import DEFAULT_REPLY_WINDOW, OutboundContactStore
|
|
157
|
+
from aap.discovery import (
|
|
158
|
+
build_introduction_response_envelope,
|
|
159
|
+
extract_searcher_identities,
|
|
160
|
+
query_discovery,
|
|
161
|
+
)
|
|
162
|
+
from aap.stores.pending_introductions import PendingIntroductionRow, PendingIntroductions
|
|
163
|
+
from aap.stores.pending_proposals import (
|
|
164
|
+
PendingInbound,
|
|
165
|
+
PendingOutbound,
|
|
166
|
+
PendingProposalStore,
|
|
167
|
+
)
|
|
168
|
+
from aap.stores.verification_flow import PendingVerifications, PendingVerificationRow
|
|
169
|
+
from aap.stores.service_request_groups import ServiceRequestGroupIndex
|
|
170
|
+
from aap.stores.service_requests import (
|
|
171
|
+
ServiceRequestStore,
|
|
172
|
+
StoredServiceRequest,
|
|
173
|
+
StoredServiceResponse,
|
|
174
|
+
)
|
|
175
|
+
from aap.version import __version__
|
|
176
|
+
|
|
177
|
+
__all__ = [
|
|
178
|
+
"__version__",
|
|
179
|
+
"AAPClient",
|
|
180
|
+
"AAPClientError",
|
|
181
|
+
"AgentCardKeyChanged",
|
|
182
|
+
"Address",
|
|
183
|
+
"AgentCard",
|
|
184
|
+
"AttestationStore",
|
|
185
|
+
"CHAT_PAYLOAD_TYPE",
|
|
186
|
+
"Conversation",
|
|
187
|
+
"ConversationEventRecord",
|
|
188
|
+
"ConversationPolicyError",
|
|
189
|
+
"ConversationStore",
|
|
190
|
+
"DEFAULT_LIFETIME_DAYS",
|
|
191
|
+
"DEFAULT_ENVELOPE_MAX_AGE_SECONDS",
|
|
192
|
+
"DEFAULT_FUTURE_SKEW_SECONDS",
|
|
193
|
+
"DEFAULT_REPLY_WINDOW",
|
|
194
|
+
"DEFAULT_TRUSTED_VERIFIERS_URL",
|
|
195
|
+
"TRUSTED_VERIFIERS_ISSUER",
|
|
196
|
+
"TRUSTED_VERIFIERS_PAYLOAD_TYPE",
|
|
197
|
+
"DIDWebError",
|
|
198
|
+
"KeyPinChanged",
|
|
199
|
+
"KeyPins",
|
|
200
|
+
"DiscoveryIntroductionRequest",
|
|
201
|
+
"DiscoveryIntroductionResponse",
|
|
202
|
+
"DiscoveryQueryResponse",
|
|
203
|
+
"Envelope",
|
|
204
|
+
"EnvelopeError",
|
|
205
|
+
"EnvelopePolicyError",
|
|
206
|
+
"EnvelopeReplayCache",
|
|
207
|
+
"ENCRYPTED_ENVELOPE_TYPE",
|
|
208
|
+
"EncryptedEnvelope",
|
|
209
|
+
"EncryptionError",
|
|
210
|
+
"FollowupGrantStore",
|
|
211
|
+
"GroupComplete",
|
|
212
|
+
"GroupInvitation",
|
|
213
|
+
"GroupLeave",
|
|
214
|
+
"GroupMembershipUpdate",
|
|
215
|
+
"HIGH_RISK_LIFETIME_DAYS",
|
|
216
|
+
"HPKE_ALGORITHM",
|
|
217
|
+
"IdentityBinding",
|
|
218
|
+
"IdentityBindingStore",
|
|
219
|
+
"IdentityFile",
|
|
220
|
+
"InsecureTransportError",
|
|
221
|
+
"InboundPolicyError",
|
|
222
|
+
"KeyChangeRejected",
|
|
223
|
+
"OutboundContactStore",
|
|
224
|
+
"PendingConsent",
|
|
225
|
+
"PendingIntroductionRow",
|
|
226
|
+
"PendingIntroductions",
|
|
227
|
+
"PendingInbound",
|
|
228
|
+
"PendingOutbound",
|
|
229
|
+
"PendingProposalStore",
|
|
230
|
+
"PendingResponses",
|
|
231
|
+
"PendingVerificationRow",
|
|
232
|
+
"PendingVerifications",
|
|
233
|
+
"ROUTING_ENVELOPE_TYPE",
|
|
234
|
+
"SERVICE_CATALOG_PAYLOAD_TYPE",
|
|
235
|
+
"RelationshipAccept",
|
|
236
|
+
"RelationshipDecline",
|
|
237
|
+
"RelationshipProposal",
|
|
238
|
+
"RelationshipRecord",
|
|
239
|
+
"RelationshipRevocationRecord",
|
|
240
|
+
"RelationshipRevoke",
|
|
241
|
+
"RelationshipStore",
|
|
242
|
+
"ServiceCatalog",
|
|
243
|
+
"ServiceCatalogCache",
|
|
244
|
+
"ServiceCatalogPayload",
|
|
245
|
+
"ServiceDefinition",
|
|
246
|
+
"ServiceFollowup",
|
|
247
|
+
"ServiceFollowupGrant",
|
|
248
|
+
"ServiceRequest",
|
|
249
|
+
"ServiceRequestGroupIndex",
|
|
250
|
+
"ServiceRequestStore",
|
|
251
|
+
"ServiceResponse",
|
|
252
|
+
"ServiceResponseStatus",
|
|
253
|
+
"StoredAttestation",
|
|
254
|
+
"StoredFollowupGrant",
|
|
255
|
+
"StoredServiceRequest",
|
|
256
|
+
"StoredServiceResponse",
|
|
257
|
+
"TrustListCache",
|
|
258
|
+
"UnsupportedPayloadType",
|
|
259
|
+
"VALID_RELATIONSHIP_TYPES",
|
|
260
|
+
"ValidationFailure",
|
|
261
|
+
"VerificationAttestation",
|
|
262
|
+
"VerifyConfirmResponse",
|
|
263
|
+
"VerifyStartResponse",
|
|
264
|
+
"VerifiedIdentity",
|
|
265
|
+
"VerifierClientError",
|
|
266
|
+
"VerifierPubkeyCache",
|
|
267
|
+
"VerifierTrustListEntry",
|
|
268
|
+
"VerifyStartResult",
|
|
269
|
+
"ValidatedChat",
|
|
270
|
+
"ValidatedEnvelope",
|
|
271
|
+
"WILDCARD",
|
|
272
|
+
"broadcast_to_conversation",
|
|
273
|
+
"build_chat_envelope",
|
|
274
|
+
"build_followup_envelope",
|
|
275
|
+
"build_followup_grant_envelope",
|
|
276
|
+
"build_group_complete_envelope",
|
|
277
|
+
"build_group_invitation_envelope",
|
|
278
|
+
"build_group_leave_envelope",
|
|
279
|
+
"build_group_membership_update_envelope",
|
|
280
|
+
"build_introduction_response_envelope",
|
|
281
|
+
"build_relationship_accept_envelope",
|
|
282
|
+
"build_relationship_decline_envelope",
|
|
283
|
+
"build_relationship_proposal_envelope",
|
|
284
|
+
"build_relationship_revoke_envelope",
|
|
285
|
+
"build_service_catalog_envelope",
|
|
286
|
+
"build_service_request_envelope",
|
|
287
|
+
"build_service_response_envelope",
|
|
288
|
+
"confirm_email_verification",
|
|
289
|
+
"confirm_sms_verification",
|
|
290
|
+
"decode_b64url",
|
|
291
|
+
"did_web_document_url",
|
|
292
|
+
"did_web_domain",
|
|
293
|
+
"decrypt_envelope",
|
|
294
|
+
"derive_encryption_keypair",
|
|
295
|
+
"encode_b64url",
|
|
296
|
+
"encrypt_envelope",
|
|
297
|
+
"encryption_key_id",
|
|
298
|
+
"encryption_public_from_private",
|
|
299
|
+
"envelope_replay_key",
|
|
300
|
+
"extract_searcher_identities",
|
|
301
|
+
"generate_keypair",
|
|
302
|
+
"generate_encryption_keypair",
|
|
303
|
+
"is_high_risk_scope",
|
|
304
|
+
"load_or_generate",
|
|
305
|
+
"parse_iso_duration",
|
|
306
|
+
"parse_rfc3339",
|
|
307
|
+
"parse_trusted_verifiers",
|
|
308
|
+
"query_discovery",
|
|
309
|
+
"require_secure_url",
|
|
310
|
+
"resolve_did_web_key",
|
|
311
|
+
"seed_to_keypair",
|
|
312
|
+
"should_auto_renew",
|
|
313
|
+
"sign",
|
|
314
|
+
"start_email_verification",
|
|
315
|
+
"start_sms_verification",
|
|
316
|
+
"token_lifetime_days",
|
|
317
|
+
"trusted_verifiers_supporting",
|
|
318
|
+
"unwrap_chat_envelope",
|
|
319
|
+
"validate_inbound_chat",
|
|
320
|
+
"validate_inbound_envelope",
|
|
321
|
+
"validate_service_payload",
|
|
322
|
+
"validate_envelope_iat",
|
|
323
|
+
"verifier_relay_address",
|
|
324
|
+
"verify",
|
|
325
|
+
"verify_envelope",
|
|
326
|
+
"wrap_routing_envelope",
|
|
327
|
+
]
|
aap/address.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""AAP address: <localpart>^<domain>.
|
|
2
|
+
|
|
3
|
+
Localpart accepts ASCII alphanumerics, '.', '-', '_'. Case-insensitive
|
|
4
|
+
— normalised to lowercase on parse so ``Chris-work`` and ``chris-work``
|
|
5
|
+
route to the same agent. Max length 64.
|
|
6
|
+
|
|
7
|
+
Domain accepts ASCII alphanumerics, '.', '-'. Normalised to lowercase
|
|
8
|
+
on parse (DNS labels are case-insensitive). Max length 253.
|
|
9
|
+
|
|
10
|
+
The ``^`` separator was chosen because it (a) is easy to type
|
|
11
|
+
(shift+6 on the number row), (b) is shell- and URL-safe in practice,
|
|
12
|
+
and (c) appears in neither the localpart nor the domain grammar, so
|
|
13
|
+
the split is unambiguous. The format intentionally does not resemble
|
|
14
|
+
an email address.
|
|
15
|
+
|
|
16
|
+
DNS-level checks (label length, leading/trailing hyphen, IDN punycode)
|
|
17
|
+
happen at resolution time, not parse time.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
_LOCALPART_MAX = 64
|
|
23
|
+
_DOMAIN_MAX = 253
|
|
24
|
+
# `+` is allowed in the localpart so derivative addresses like
|
|
25
|
+
# `chris+work^…` parse cleanly. The address-claim system uses the
|
|
26
|
+
# part before the first `+` as the base; for parsing purposes here `+`
|
|
27
|
+
# is just another permitted character.
|
|
28
|
+
_ALLOWED_LOCALPART_EXTRA = frozenset(".-_+")
|
|
29
|
+
_ALLOWED_DOMAIN_EXTRA = frozenset(".-")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _valid_localpart_char(c: str) -> bool:
|
|
33
|
+
return (c.isalnum() and c.isascii()) or (c in _ALLOWED_LOCALPART_EXTRA)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _valid_domain_char(c: str) -> bool:
|
|
37
|
+
return (c.isalnum() and c.isascii()) or (c in _ALLOWED_DOMAIN_EXTRA)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Address:
|
|
42
|
+
localpart: str
|
|
43
|
+
domain: str
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def parse(cls, s: str) -> "Address":
|
|
47
|
+
if "^" not in s:
|
|
48
|
+
raise ValueError(f"AAP address must contain '^': {s!r}")
|
|
49
|
+
localpart, domain = s.rsplit("^", 1)
|
|
50
|
+
if not localpart:
|
|
51
|
+
raise ValueError("localpart cannot be empty")
|
|
52
|
+
if not domain:
|
|
53
|
+
raise ValueError("domain cannot be empty")
|
|
54
|
+
if len(localpart) > _LOCALPART_MAX:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"localpart too long ({len(localpart)} > {_LOCALPART_MAX})"
|
|
57
|
+
)
|
|
58
|
+
if not all(_valid_localpart_char(c) for c in localpart):
|
|
59
|
+
raise ValueError(f"localpart contains invalid characters: {localpart!r}")
|
|
60
|
+
localpart = localpart.lower()
|
|
61
|
+
if len(domain) > _DOMAIN_MAX:
|
|
62
|
+
raise ValueError(f"domain too long ({len(domain)} > {_DOMAIN_MAX})")
|
|
63
|
+
domain = domain.lower()
|
|
64
|
+
if not all(_valid_domain_char(c) for c in domain):
|
|
65
|
+
raise ValueError(f"domain contains invalid characters: {domain!r}")
|
|
66
|
+
return cls(localpart=localpart, domain=domain)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def parse_user_input(
|
|
70
|
+
cls,
|
|
71
|
+
s: str,
|
|
72
|
+
*,
|
|
73
|
+
default_domain: str = "agentaddress.org",
|
|
74
|
+
) -> "Address":
|
|
75
|
+
"""Parse human/LLM-entered address text.
|
|
76
|
+
|
|
77
|
+
A trailing caret is shorthand for the hosted Agent Address namespace:
|
|
78
|
+
``chris^`` expands to ``chris^agentaddress.org``. The expanded value is
|
|
79
|
+
then passed through strict parsing so protocol validation remains in one
|
|
80
|
+
place.
|
|
81
|
+
"""
|
|
82
|
+
value = s.strip()
|
|
83
|
+
if value.endswith("^"):
|
|
84
|
+
value = f"{value}{default_domain}"
|
|
85
|
+
return cls.parse(value)
|
|
86
|
+
|
|
87
|
+
def __str__(self) -> str:
|
|
88
|
+
return f"{self.localpart}^{self.domain}"
|