agentguard-python-sdk 0.2.2__py3-none-any.whl → 0.2.3__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.
- agentguard/__init__.py +57 -57
- agentguard/auth.py +20 -20
- agentguard/config.py +1 -1
- agentguard/observability.py +52 -52
- agentguard_crypto/__init__.py +1 -1
- agentguard_crypto/canonical.py +34 -34
- agentguard_crypto/models.py +108 -108
- agentguard_crypto/signing.py +76 -76
- {agentguard_python_sdk-0.2.2.dist-info → agentguard_python_sdk-0.2.3.dist-info}/METADATA +1 -1
- agentguard_python_sdk-0.2.3.dist-info/RECORD +17 -0
- {agentguard_python_sdk-0.2.2.dist-info → agentguard_python_sdk-0.2.3.dist-info}/licenses/LICENSE +21 -21
- agentguard_python_sdk-0.2.2.dist-info/RECORD +0 -17
- {agentguard_python_sdk-0.2.2.dist-info → agentguard_python_sdk-0.2.3.dist-info}/WHEEL +0 -0
- {agentguard_python_sdk-0.2.2.dist-info → agentguard_python_sdk-0.2.3.dist-info}/top_level.txt +0 -0
agentguard/__init__.py
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
from .client import AgentGuardClient
|
|
2
|
-
from .config import AgentGuardConfig
|
|
3
|
-
from .errors import (
|
|
4
|
-
AgentGuardError,
|
|
5
|
-
ValidationError,
|
|
6
|
-
TransportError,
|
|
7
|
-
AuthenticationError,
|
|
8
|
-
InvalidSignatureError,
|
|
9
|
-
ExpiredSignatureError,
|
|
10
|
-
PaymentError,
|
|
11
|
-
ReplayAttackError,
|
|
12
|
-
DuplicatePaymentError,
|
|
13
|
-
BudgetExceededError,
|
|
14
|
-
VerificationError,
|
|
15
|
-
SecurityError,
|
|
16
|
-
UnknownExecutionStateError,
|
|
17
|
-
AuthorizationRequiredError
|
|
18
|
-
)
|
|
19
|
-
from .types import (
|
|
20
|
-
AgentGuardReceipt,
|
|
21
|
-
AuditProof,
|
|
22
|
-
AuthorizationConstraints,
|
|
23
|
-
NonceContext,
|
|
24
|
-
AuthorizationMetadata,
|
|
25
|
-
DelegatedAuthorization,
|
|
26
|
-
AuthorizationReceipt,
|
|
27
|
-
RevocationReceipt
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
__version__ = "0.2.2"
|
|
31
|
-
|
|
32
|
-
__all__ = [
|
|
33
|
-
"AgentGuardClient",
|
|
34
|
-
"AgentGuardConfig",
|
|
35
|
-
"AgentGuardError",
|
|
36
|
-
"ValidationError",
|
|
37
|
-
"TransportError",
|
|
38
|
-
"AuthenticationError",
|
|
39
|
-
"InvalidSignatureError",
|
|
40
|
-
"ExpiredSignatureError",
|
|
41
|
-
"PaymentError",
|
|
42
|
-
"ReplayAttackError",
|
|
43
|
-
"DuplicatePaymentError",
|
|
44
|
-
"BudgetExceededError",
|
|
45
|
-
"VerificationError",
|
|
46
|
-
"SecurityError",
|
|
47
|
-
"UnknownExecutionStateError",
|
|
48
|
-
"AuthorizationRequiredError",
|
|
49
|
-
"AgentGuardReceipt",
|
|
50
|
-
"AuditProof",
|
|
51
|
-
"AuthorizationConstraints",
|
|
52
|
-
"NonceContext",
|
|
53
|
-
"AuthorizationMetadata",
|
|
54
|
-
"DelegatedAuthorization",
|
|
55
|
-
"AuthorizationReceipt",
|
|
56
|
-
"RevocationReceipt",
|
|
57
|
-
]
|
|
1
|
+
from .client import AgentGuardClient
|
|
2
|
+
from .config import AgentGuardConfig
|
|
3
|
+
from .errors import (
|
|
4
|
+
AgentGuardError,
|
|
5
|
+
ValidationError,
|
|
6
|
+
TransportError,
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
InvalidSignatureError,
|
|
9
|
+
ExpiredSignatureError,
|
|
10
|
+
PaymentError,
|
|
11
|
+
ReplayAttackError,
|
|
12
|
+
DuplicatePaymentError,
|
|
13
|
+
BudgetExceededError,
|
|
14
|
+
VerificationError,
|
|
15
|
+
SecurityError,
|
|
16
|
+
UnknownExecutionStateError,
|
|
17
|
+
AuthorizationRequiredError
|
|
18
|
+
)
|
|
19
|
+
from .types import (
|
|
20
|
+
AgentGuardReceipt,
|
|
21
|
+
AuditProof,
|
|
22
|
+
AuthorizationConstraints,
|
|
23
|
+
NonceContext,
|
|
24
|
+
AuthorizationMetadata,
|
|
25
|
+
DelegatedAuthorization,
|
|
26
|
+
AuthorizationReceipt,
|
|
27
|
+
RevocationReceipt
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = "0.2.2"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AgentGuardClient",
|
|
34
|
+
"AgentGuardConfig",
|
|
35
|
+
"AgentGuardError",
|
|
36
|
+
"ValidationError",
|
|
37
|
+
"TransportError",
|
|
38
|
+
"AuthenticationError",
|
|
39
|
+
"InvalidSignatureError",
|
|
40
|
+
"ExpiredSignatureError",
|
|
41
|
+
"PaymentError",
|
|
42
|
+
"ReplayAttackError",
|
|
43
|
+
"DuplicatePaymentError",
|
|
44
|
+
"BudgetExceededError",
|
|
45
|
+
"VerificationError",
|
|
46
|
+
"SecurityError",
|
|
47
|
+
"UnknownExecutionStateError",
|
|
48
|
+
"AuthorizationRequiredError",
|
|
49
|
+
"AgentGuardReceipt",
|
|
50
|
+
"AuditProof",
|
|
51
|
+
"AuthorizationConstraints",
|
|
52
|
+
"NonceContext",
|
|
53
|
+
"AuthorizationMetadata",
|
|
54
|
+
"DelegatedAuthorization",
|
|
55
|
+
"AuthorizationReceipt",
|
|
56
|
+
"RevocationReceipt",
|
|
57
|
+
]
|
agentguard/auth.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import unicodedata
|
|
3
|
-
import hashlib
|
|
4
|
-
import base64
|
|
5
|
-
import time
|
|
6
|
-
from typing import Dict, Any
|
|
7
|
-
import nacl.signing
|
|
8
|
-
import nacl.encoding
|
|
9
|
-
|
|
10
|
-
from agentguard_crypto.signing import sign_payload
|
|
11
|
-
|
|
12
|
-
def sign_request(payload: Any, private_key_b64: str) -> str:
|
|
13
|
-
"""
|
|
14
|
-
Signs a request using the shared cryptographic source of truth.
|
|
15
|
-
Payload MUST be a strictly typed canonical model.
|
|
16
|
-
"""
|
|
17
|
-
if isinstance(payload, dict):
|
|
18
|
-
raise TypeError("Dictionary payloads are strictly prohibited. You must use a Canonical Payload model.")
|
|
19
|
-
|
|
20
|
-
return sign_payload(payload, private_key_b64)
|
|
1
|
+
import json
|
|
2
|
+
import unicodedata
|
|
3
|
+
import hashlib
|
|
4
|
+
import base64
|
|
5
|
+
import time
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
import nacl.signing
|
|
8
|
+
import nacl.encoding
|
|
9
|
+
|
|
10
|
+
from agentguard_crypto.signing import sign_payload
|
|
11
|
+
|
|
12
|
+
def sign_request(payload: Any, private_key_b64: str) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Signs a request using the shared cryptographic source of truth.
|
|
15
|
+
Payload MUST be a strictly typed canonical model.
|
|
16
|
+
"""
|
|
17
|
+
if isinstance(payload, dict):
|
|
18
|
+
raise TypeError("Dictionary payloads are strictly prohibited. You must use a Canonical Payload model.")
|
|
19
|
+
|
|
20
|
+
return sign_payload(payload, private_key_b64)
|
agentguard/config.py
CHANGED
agentguard/observability.py
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import time
|
|
2
|
-
import json
|
|
3
|
-
import uuid
|
|
4
|
-
import logging
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from typing import Optional, Dict, Any
|
|
7
|
-
|
|
8
|
-
# [PRIORITY 9] SDK OBSERVABILITY CORE
|
|
9
|
-
|
|
10
|
-
class Span:
|
|
11
|
-
def __init__(self, name: str, trace_id: str, parent_id: Optional[str] = None):
|
|
12
|
-
self.name = name
|
|
13
|
-
self.trace_id = trace_id
|
|
14
|
-
self.span_id = str(uuid.uuid4())[:8]
|
|
15
|
-
self.parent_id = parent_id
|
|
16
|
-
self.start_time = time.time()
|
|
17
|
-
self.metadata = {}
|
|
18
|
-
|
|
19
|
-
def set_attribute(self, key: str, value: Any):
|
|
20
|
-
self.metadata[key] = value
|
|
21
|
-
|
|
22
|
-
def end(self, error: Optional[str] = None):
|
|
23
|
-
duration = (time.time() - self.start_time) * 1000
|
|
24
|
-
log_data = {
|
|
25
|
-
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
26
|
-
"level": "INFO" if not error else "ERROR",
|
|
27
|
-
"service": "agentguard-sdk",
|
|
28
|
-
"event": "span_end",
|
|
29
|
-
"span_name": self.name,
|
|
30
|
-
"span_id": self.span_id,
|
|
31
|
-
"parent_id": self.parent_id,
|
|
32
|
-
"trace_id": self.trace_id,
|
|
33
|
-
"duration_ms": duration,
|
|
34
|
-
"error": error,
|
|
35
|
-
"metadata": self.metadata
|
|
36
|
-
}
|
|
37
|
-
logging.getLogger("agentguard-sdk").info(json.dumps(log_data))
|
|
38
|
-
|
|
39
|
-
def start_span(name: str, trace_id: str, parent_id: Optional[str] = None) -> Span:
|
|
40
|
-
span = Span(name, trace_id, parent_id)
|
|
41
|
-
log_data = {
|
|
42
|
-
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
43
|
-
"level": "INFO",
|
|
44
|
-
"service": "agentguard-sdk",
|
|
45
|
-
"event": "span_start",
|
|
46
|
-
"span_name": name,
|
|
47
|
-
"span_id": span.span_id,
|
|
48
|
-
"parent_id": parent_id,
|
|
49
|
-
"trace_id": trace_id
|
|
50
|
-
}
|
|
51
|
-
logging.getLogger("agentguard-sdk").info(json.dumps(log_data))
|
|
52
|
-
return span
|
|
1
|
+
import time
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
|
|
8
|
+
# [PRIORITY 9] SDK OBSERVABILITY CORE
|
|
9
|
+
|
|
10
|
+
class Span:
|
|
11
|
+
def __init__(self, name: str, trace_id: str, parent_id: Optional[str] = None):
|
|
12
|
+
self.name = name
|
|
13
|
+
self.trace_id = trace_id
|
|
14
|
+
self.span_id = str(uuid.uuid4())[:8]
|
|
15
|
+
self.parent_id = parent_id
|
|
16
|
+
self.start_time = time.time()
|
|
17
|
+
self.metadata = {}
|
|
18
|
+
|
|
19
|
+
def set_attribute(self, key: str, value: Any):
|
|
20
|
+
self.metadata[key] = value
|
|
21
|
+
|
|
22
|
+
def end(self, error: Optional[str] = None):
|
|
23
|
+
duration = (time.time() - self.start_time) * 1000
|
|
24
|
+
log_data = {
|
|
25
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
26
|
+
"level": "INFO" if not error else "ERROR",
|
|
27
|
+
"service": "agentguard-sdk",
|
|
28
|
+
"event": "span_end",
|
|
29
|
+
"span_name": self.name,
|
|
30
|
+
"span_id": self.span_id,
|
|
31
|
+
"parent_id": self.parent_id,
|
|
32
|
+
"trace_id": self.trace_id,
|
|
33
|
+
"duration_ms": duration,
|
|
34
|
+
"error": error,
|
|
35
|
+
"metadata": self.metadata
|
|
36
|
+
}
|
|
37
|
+
logging.getLogger("agentguard-sdk").info(json.dumps(log_data))
|
|
38
|
+
|
|
39
|
+
def start_span(name: str, trace_id: str, parent_id: Optional[str] = None) -> Span:
|
|
40
|
+
span = Span(name, trace_id, parent_id)
|
|
41
|
+
log_data = {
|
|
42
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
43
|
+
"level": "INFO",
|
|
44
|
+
"service": "agentguard-sdk",
|
|
45
|
+
"event": "span_start",
|
|
46
|
+
"span_name": name,
|
|
47
|
+
"span_id": span.span_id,
|
|
48
|
+
"parent_id": parent_id,
|
|
49
|
+
"trace_id": trace_id
|
|
50
|
+
}
|
|
51
|
+
logging.getLogger("agentguard-sdk").info(json.dumps(log_data))
|
|
52
|
+
return span
|
agentguard_crypto/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
# Empty init for agentguard_crypto package
|
|
1
|
+
# Empty init for agentguard_crypto package
|
agentguard_crypto/canonical.py
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import unicodedata
|
|
3
|
-
from typing import Dict, Any
|
|
4
|
-
|
|
5
|
-
def canonicalize_payload(payload: Dict[str, Any]) -> bytes:
|
|
6
|
-
"""
|
|
7
|
-
Industry-Grade Deterministic Canonicalization.
|
|
8
|
-
1. Recursive NFC Normalization.
|
|
9
|
-
2. Recursive None-Stripping (Parity between null/unset).
|
|
10
|
-
3. Alphabetical Key Sorting.
|
|
11
|
-
4. Compact JSON (separators=(',', ':')).
|
|
12
|
-
5. UTF-8 Encoding.
|
|
13
|
-
"""
|
|
14
|
-
def normalize_rec(obj):
|
|
15
|
-
if isinstance(obj, str):
|
|
16
|
-
# Strict NFC Normalization for inconsistent client environments
|
|
17
|
-
return unicodedata.normalize("NFC", obj)
|
|
18
|
-
if isinstance(obj, dict):
|
|
19
|
-
# Sort keys and discard None values for cryptographic parity
|
|
20
|
-
return {normalize_rec(k): normalize_rec(v) for k, v in obj.items() if v is not None}
|
|
21
|
-
if isinstance(obj, list):
|
|
22
|
-
return [normalize_rec(i) for i in obj]
|
|
23
|
-
return obj
|
|
24
|
-
|
|
25
|
-
norm_payload = normalize_rec(payload)
|
|
26
|
-
|
|
27
|
-
# Deterministic JSON serialization
|
|
28
|
-
canonical_str = json.dumps(
|
|
29
|
-
norm_payload,
|
|
30
|
-
sort_keys=True,
|
|
31
|
-
separators=(",", ":"),
|
|
32
|
-
ensure_ascii=False
|
|
33
|
-
)
|
|
34
|
-
return canonical_str.encode("utf-8")
|
|
1
|
+
import json
|
|
2
|
+
import unicodedata
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
def canonicalize_payload(payload: Dict[str, Any]) -> bytes:
|
|
6
|
+
"""
|
|
7
|
+
Industry-Grade Deterministic Canonicalization.
|
|
8
|
+
1. Recursive NFC Normalization.
|
|
9
|
+
2. Recursive None-Stripping (Parity between null/unset).
|
|
10
|
+
3. Alphabetical Key Sorting.
|
|
11
|
+
4. Compact JSON (separators=(',', ':')).
|
|
12
|
+
5. UTF-8 Encoding.
|
|
13
|
+
"""
|
|
14
|
+
def normalize_rec(obj):
|
|
15
|
+
if isinstance(obj, str):
|
|
16
|
+
# Strict NFC Normalization for inconsistent client environments
|
|
17
|
+
return unicodedata.normalize("NFC", obj)
|
|
18
|
+
if isinstance(obj, dict):
|
|
19
|
+
# Sort keys and discard None values for cryptographic parity
|
|
20
|
+
return {normalize_rec(k): normalize_rec(v) for k, v in obj.items() if v is not None}
|
|
21
|
+
if isinstance(obj, list):
|
|
22
|
+
return [normalize_rec(i) for i in obj]
|
|
23
|
+
return obj
|
|
24
|
+
|
|
25
|
+
norm_payload = normalize_rec(payload)
|
|
26
|
+
|
|
27
|
+
# Deterministic JSON serialization
|
|
28
|
+
canonical_str = json.dumps(
|
|
29
|
+
norm_payload,
|
|
30
|
+
sort_keys=True,
|
|
31
|
+
separators=(",", ":"),
|
|
32
|
+
ensure_ascii=False
|
|
33
|
+
)
|
|
34
|
+
return canonical_str.encode("utf-8")
|
agentguard_crypto/models.py
CHANGED
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
from pydantic import BaseModel, Field, StrictInt
|
|
3
|
-
from .canonical import canonicalize_payload
|
|
4
|
-
|
|
5
|
-
SCHEMA_VERSION = 1
|
|
6
|
-
|
|
7
|
-
class CanonicalPayloadMixin:
|
|
8
|
-
"""
|
|
9
|
-
PROTOCOL LAYER: Single Serialization Authority.
|
|
10
|
-
|
|
11
|
-
ALL serialization MUST derive from _serialize().
|
|
12
|
-
Direct model_dump() calls are FORBIDDEN in protocol code.
|
|
13
|
-
|
|
14
|
-
Invariant enforced by construction:
|
|
15
|
-
wire_payload == signed_payload == verified_payload
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
def _serialize(self) -> dict:
|
|
19
|
-
"""SINGLE INTERNAL SERIALIZATION SOURCE.
|
|
20
|
-
Every external serialization method MUST derive from this.
|
|
21
|
-
Uses exclude_unset=True to prevent default injection drift."""
|
|
22
|
-
return self.model_dump(exclude_unset=True)
|
|
23
|
-
|
|
24
|
-
def to_canonical_bytes(self) -> bytes:
|
|
25
|
-
"""Returns deterministic canonical bytes for signing/verification."""
|
|
26
|
-
return canonicalize_payload(self._serialize())
|
|
27
|
-
|
|
28
|
-
def to_wire_dict(self) -> dict:
|
|
29
|
-
"""Returns the EXACT dict for HTTP transport.
|
|
30
|
-
Guaranteed identical field set to what is signed/verified."""
|
|
31
|
-
return self._serialize()
|
|
32
|
-
|
|
33
|
-
# Backward compatibility alias (used by signing.py, consent.py, payment.py)
|
|
34
|
-
get_canonical_bytes = to_canonical_bytes
|
|
35
|
-
|
|
36
|
-
class CanonicalPaymentPayload(BaseModel, CanonicalPayloadMixin):
|
|
37
|
-
"""
|
|
38
|
-
SINGLE SOURCE OF TRUTH (Phase 2).
|
|
39
|
-
Represents the exact fields that are signed and verified for Payment.
|
|
40
|
-
"""
|
|
41
|
-
# Core Identity
|
|
42
|
-
principal_id: str
|
|
43
|
-
resource_url: str
|
|
44
|
-
purpose: str
|
|
45
|
-
microalgos: StrictInt = Field(gt=0)
|
|
46
|
-
|
|
47
|
-
# Target
|
|
48
|
-
receiver_address: Optional[str] = None
|
|
49
|
-
receiver: Optional[str] = None
|
|
50
|
-
|
|
51
|
-
# Cryptographic Nonces
|
|
52
|
-
nonce: str
|
|
53
|
-
timestamp: int
|
|
54
|
-
|
|
55
|
-
# Authorization Context
|
|
56
|
-
authorization_id: Optional[str] = None
|
|
57
|
-
consent_hash: str
|
|
58
|
-
|
|
59
|
-
# Correlation & Idempotency
|
|
60
|
-
idempotency_key: str
|
|
61
|
-
correlation_id: str
|
|
62
|
-
session_id: Optional[str] = None
|
|
63
|
-
|
|
64
|
-
# Fiduciary Metadata (Phase A1)
|
|
65
|
-
fiduciary_name: Optional[str] = None
|
|
66
|
-
fiduciary_version: Optional[str] = None
|
|
67
|
-
hash_version: Optional[int] = 1
|
|
68
|
-
|
|
69
|
-
# Versioning (Phase 5)
|
|
70
|
-
schema_version: int = Field(default=SCHEMA_VERSION)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class CanonicalAuthorizationPayload(BaseModel, CanonicalPayloadMixin):
|
|
74
|
-
"""
|
|
75
|
-
STRICTLY contains ONLY authorization lifecycle fields.
|
|
76
|
-
"""
|
|
77
|
-
principal_id: str
|
|
78
|
-
logic_sig_b64: str
|
|
79
|
-
max_amount: StrictInt = Field(gt=0)
|
|
80
|
-
expires_at_unix: StrictInt
|
|
81
|
-
receiver: Optional[str] = None
|
|
82
|
-
purpose_hash: Optional[str] = None
|
|
83
|
-
authorization_id: Optional[str] = None
|
|
84
|
-
timestamp: int
|
|
85
|
-
nonce: str
|
|
86
|
-
schema_version: int = Field(default=SCHEMA_VERSION)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class CanonicalRevokePayload(BaseModel, CanonicalPayloadMixin):
|
|
90
|
-
"""
|
|
91
|
-
STRICTLY contains ONLY revocation fields.
|
|
92
|
-
"""
|
|
93
|
-
principal_id: str
|
|
94
|
-
authorization_id: str
|
|
95
|
-
timestamp: int
|
|
96
|
-
nonce: str
|
|
97
|
-
schema_version: int = Field(default=SCHEMA_VERSION)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class CanonicalRegistrationPayload(BaseModel, CanonicalPayloadMixin):
|
|
101
|
-
"""
|
|
102
|
-
STRICTLY contains ONLY registration fields.
|
|
103
|
-
"""
|
|
104
|
-
principal_id: str
|
|
105
|
-
budget_microalgos: Optional[StrictInt] = None
|
|
106
|
-
timestamp: int
|
|
107
|
-
nonce: str
|
|
108
|
-
schema_version: int = Field(default=SCHEMA_VERSION)
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from pydantic import BaseModel, Field, StrictInt
|
|
3
|
+
from .canonical import canonicalize_payload
|
|
4
|
+
|
|
5
|
+
SCHEMA_VERSION = 1
|
|
6
|
+
|
|
7
|
+
class CanonicalPayloadMixin:
|
|
8
|
+
"""
|
|
9
|
+
PROTOCOL LAYER: Single Serialization Authority.
|
|
10
|
+
|
|
11
|
+
ALL serialization MUST derive from _serialize().
|
|
12
|
+
Direct model_dump() calls are FORBIDDEN in protocol code.
|
|
13
|
+
|
|
14
|
+
Invariant enforced by construction:
|
|
15
|
+
wire_payload == signed_payload == verified_payload
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def _serialize(self) -> dict:
|
|
19
|
+
"""SINGLE INTERNAL SERIALIZATION SOURCE.
|
|
20
|
+
Every external serialization method MUST derive from this.
|
|
21
|
+
Uses exclude_unset=True to prevent default injection drift."""
|
|
22
|
+
return self.model_dump(exclude_unset=True)
|
|
23
|
+
|
|
24
|
+
def to_canonical_bytes(self) -> bytes:
|
|
25
|
+
"""Returns deterministic canonical bytes for signing/verification."""
|
|
26
|
+
return canonicalize_payload(self._serialize())
|
|
27
|
+
|
|
28
|
+
def to_wire_dict(self) -> dict:
|
|
29
|
+
"""Returns the EXACT dict for HTTP transport.
|
|
30
|
+
Guaranteed identical field set to what is signed/verified."""
|
|
31
|
+
return self._serialize()
|
|
32
|
+
|
|
33
|
+
# Backward compatibility alias (used by signing.py, consent.py, payment.py)
|
|
34
|
+
get_canonical_bytes = to_canonical_bytes
|
|
35
|
+
|
|
36
|
+
class CanonicalPaymentPayload(BaseModel, CanonicalPayloadMixin):
|
|
37
|
+
"""
|
|
38
|
+
SINGLE SOURCE OF TRUTH (Phase 2).
|
|
39
|
+
Represents the exact fields that are signed and verified for Payment.
|
|
40
|
+
"""
|
|
41
|
+
# Core Identity
|
|
42
|
+
principal_id: str
|
|
43
|
+
resource_url: str
|
|
44
|
+
purpose: str
|
|
45
|
+
microalgos: StrictInt = Field(gt=0)
|
|
46
|
+
|
|
47
|
+
# Target
|
|
48
|
+
receiver_address: Optional[str] = None
|
|
49
|
+
receiver: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
# Cryptographic Nonces
|
|
52
|
+
nonce: str
|
|
53
|
+
timestamp: int
|
|
54
|
+
|
|
55
|
+
# Authorization Context
|
|
56
|
+
authorization_id: Optional[str] = None
|
|
57
|
+
consent_hash: str
|
|
58
|
+
|
|
59
|
+
# Correlation & Idempotency
|
|
60
|
+
idempotency_key: str
|
|
61
|
+
correlation_id: str
|
|
62
|
+
session_id: Optional[str] = None
|
|
63
|
+
|
|
64
|
+
# Fiduciary Metadata (Phase A1)
|
|
65
|
+
fiduciary_name: Optional[str] = None
|
|
66
|
+
fiduciary_version: Optional[str] = None
|
|
67
|
+
hash_version: Optional[int] = 1
|
|
68
|
+
|
|
69
|
+
# Versioning (Phase 5)
|
|
70
|
+
schema_version: int = Field(default=SCHEMA_VERSION)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CanonicalAuthorizationPayload(BaseModel, CanonicalPayloadMixin):
|
|
74
|
+
"""
|
|
75
|
+
STRICTLY contains ONLY authorization lifecycle fields.
|
|
76
|
+
"""
|
|
77
|
+
principal_id: str
|
|
78
|
+
logic_sig_b64: str
|
|
79
|
+
max_amount: StrictInt = Field(gt=0)
|
|
80
|
+
expires_at_unix: StrictInt
|
|
81
|
+
receiver: Optional[str] = None
|
|
82
|
+
purpose_hash: Optional[str] = None
|
|
83
|
+
authorization_id: Optional[str] = None
|
|
84
|
+
timestamp: int
|
|
85
|
+
nonce: str
|
|
86
|
+
schema_version: int = Field(default=SCHEMA_VERSION)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CanonicalRevokePayload(BaseModel, CanonicalPayloadMixin):
|
|
90
|
+
"""
|
|
91
|
+
STRICTLY contains ONLY revocation fields.
|
|
92
|
+
"""
|
|
93
|
+
principal_id: str
|
|
94
|
+
authorization_id: str
|
|
95
|
+
timestamp: int
|
|
96
|
+
nonce: str
|
|
97
|
+
schema_version: int = Field(default=SCHEMA_VERSION)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CanonicalRegistrationPayload(BaseModel, CanonicalPayloadMixin):
|
|
101
|
+
"""
|
|
102
|
+
STRICTLY contains ONLY registration fields.
|
|
103
|
+
"""
|
|
104
|
+
principal_id: str
|
|
105
|
+
budget_microalgos: Optional[StrictInt] = None
|
|
106
|
+
timestamp: int
|
|
107
|
+
nonce: str
|
|
108
|
+
schema_version: int = Field(default=SCHEMA_VERSION)
|
agentguard_crypto/signing.py
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import nacl.signing
|
|
3
|
-
import nacl.encoding
|
|
4
|
-
from typing import Any
|
|
5
|
-
from .models import CanonicalPaymentPayload
|
|
6
|
-
|
|
7
|
-
def sign_payload(payload: Any, private_key_b64: str) -> str:
|
|
8
|
-
"""
|
|
9
|
-
Signs a CanonicalPaymentPayload (Phase 4).
|
|
10
|
-
Expects private_key_b64 (32-byte seed in b64).
|
|
11
|
-
"""
|
|
12
|
-
seed = base64.b64decode(private_key_b64)
|
|
13
|
-
if len(seed) > 32:
|
|
14
|
-
seed = seed[:32]
|
|
15
|
-
|
|
16
|
-
signing_key = nacl.signing.SigningKey(seed)
|
|
17
|
-
canonical_bytes = payload.get_canonical_bytes()
|
|
18
|
-
|
|
19
|
-
signature = signing_key.sign(canonical_bytes)
|
|
20
|
-
return base64.b64encode(signature.signature).decode("utf-8")
|
|
21
|
-
|
|
22
|
-
def verify_signature(payload: Any, signature_b64: str, wallet_address: str) -> bool:
|
|
23
|
-
"""
|
|
24
|
-
Verifies a CanonicalPaymentPayload signature (Phase 4).
|
|
25
|
-
wallet_address is used as the public key.
|
|
26
|
-
"""
|
|
27
|
-
from algosdk import encoding
|
|
28
|
-
import nacl.exceptions
|
|
29
|
-
|
|
30
|
-
# 1. Get canonical bytes
|
|
31
|
-
canonical_bytes = payload.get_canonical_bytes()
|
|
32
|
-
|
|
33
|
-
# 2. Decode signature and key
|
|
34
|
-
sig_bytes = base64.b64decode(signature_b64)
|
|
35
|
-
pub_key_bytes = encoding.decode_address(wallet_address)
|
|
36
|
-
verify_key = nacl.signing.VerifyKey(pub_key_bytes)
|
|
37
|
-
|
|
38
|
-
# 3. Verify
|
|
39
|
-
try:
|
|
40
|
-
verify_key.verify(canonical_bytes, sig_bytes)
|
|
41
|
-
return True
|
|
42
|
-
except nacl.exceptions.BadSignatureError:
|
|
43
|
-
return False
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def verify_raw_signature(raw_payload_dict: dict, signature_b64: str, wallet_address: str) -> bool:
|
|
47
|
-
"""
|
|
48
|
-
PRODUCTION-GRADE: Verifies signature against raw transport payload.
|
|
49
|
-
|
|
50
|
-
NO model reconstruction. Canonicalizes the raw wire dict directly.
|
|
51
|
-
This eliminates Pydantic hydration drift, default injection, and
|
|
52
|
-
triple-reconstruction attacks.
|
|
53
|
-
|
|
54
|
-
Flow: raw_wire_dict → canonicalize → verify
|
|
55
|
-
|
|
56
|
-
This is how Stripe, AWS SigV4, and WebAuthn verify signatures:
|
|
57
|
-
against exact transport bytes, never reconstructed model state.
|
|
58
|
-
"""
|
|
59
|
-
from .canonical import canonicalize_payload
|
|
60
|
-
from algosdk import encoding
|
|
61
|
-
import nacl.exceptions
|
|
62
|
-
|
|
63
|
-
# 1. Canonicalize raw transport payload directly
|
|
64
|
-
canonical_bytes = canonicalize_payload(raw_payload_dict)
|
|
65
|
-
|
|
66
|
-
# 2. Decode signature and public key
|
|
67
|
-
sig_bytes = base64.b64decode(signature_b64)
|
|
68
|
-
pub_key_bytes = encoding.decode_address(wallet_address)
|
|
69
|
-
verify_key = nacl.signing.VerifyKey(pub_key_bytes)
|
|
70
|
-
|
|
71
|
-
# 3. Verify
|
|
72
|
-
try:
|
|
73
|
-
verify_key.verify(canonical_bytes, sig_bytes)
|
|
74
|
-
return True
|
|
75
|
-
except nacl.exceptions.BadSignatureError:
|
|
76
|
-
return False
|
|
1
|
+
import base64
|
|
2
|
+
import nacl.signing
|
|
3
|
+
import nacl.encoding
|
|
4
|
+
from typing import Any
|
|
5
|
+
from .models import CanonicalPaymentPayload
|
|
6
|
+
|
|
7
|
+
def sign_payload(payload: Any, private_key_b64: str) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Signs a CanonicalPaymentPayload (Phase 4).
|
|
10
|
+
Expects private_key_b64 (32-byte seed in b64).
|
|
11
|
+
"""
|
|
12
|
+
seed = base64.b64decode(private_key_b64)
|
|
13
|
+
if len(seed) > 32:
|
|
14
|
+
seed = seed[:32]
|
|
15
|
+
|
|
16
|
+
signing_key = nacl.signing.SigningKey(seed)
|
|
17
|
+
canonical_bytes = payload.get_canonical_bytes()
|
|
18
|
+
|
|
19
|
+
signature = signing_key.sign(canonical_bytes)
|
|
20
|
+
return base64.b64encode(signature.signature).decode("utf-8")
|
|
21
|
+
|
|
22
|
+
def verify_signature(payload: Any, signature_b64: str, wallet_address: str) -> bool:
|
|
23
|
+
"""
|
|
24
|
+
Verifies a CanonicalPaymentPayload signature (Phase 4).
|
|
25
|
+
wallet_address is used as the public key.
|
|
26
|
+
"""
|
|
27
|
+
from algosdk import encoding
|
|
28
|
+
import nacl.exceptions
|
|
29
|
+
|
|
30
|
+
# 1. Get canonical bytes
|
|
31
|
+
canonical_bytes = payload.get_canonical_bytes()
|
|
32
|
+
|
|
33
|
+
# 2. Decode signature and key
|
|
34
|
+
sig_bytes = base64.b64decode(signature_b64)
|
|
35
|
+
pub_key_bytes = encoding.decode_address(wallet_address)
|
|
36
|
+
verify_key = nacl.signing.VerifyKey(pub_key_bytes)
|
|
37
|
+
|
|
38
|
+
# 3. Verify
|
|
39
|
+
try:
|
|
40
|
+
verify_key.verify(canonical_bytes, sig_bytes)
|
|
41
|
+
return True
|
|
42
|
+
except nacl.exceptions.BadSignatureError:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def verify_raw_signature(raw_payload_dict: dict, signature_b64: str, wallet_address: str) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
PRODUCTION-GRADE: Verifies signature against raw transport payload.
|
|
49
|
+
|
|
50
|
+
NO model reconstruction. Canonicalizes the raw wire dict directly.
|
|
51
|
+
This eliminates Pydantic hydration drift, default injection, and
|
|
52
|
+
triple-reconstruction attacks.
|
|
53
|
+
|
|
54
|
+
Flow: raw_wire_dict → canonicalize → verify
|
|
55
|
+
|
|
56
|
+
This is how Stripe, AWS SigV4, and WebAuthn verify signatures:
|
|
57
|
+
against exact transport bytes, never reconstructed model state.
|
|
58
|
+
"""
|
|
59
|
+
from .canonical import canonicalize_payload
|
|
60
|
+
from algosdk import encoding
|
|
61
|
+
import nacl.exceptions
|
|
62
|
+
|
|
63
|
+
# 1. Canonicalize raw transport payload directly
|
|
64
|
+
canonical_bytes = canonicalize_payload(raw_payload_dict)
|
|
65
|
+
|
|
66
|
+
# 2. Decode signature and public key
|
|
67
|
+
sig_bytes = base64.b64decode(signature_b64)
|
|
68
|
+
pub_key_bytes = encoding.decode_address(wallet_address)
|
|
69
|
+
verify_key = nacl.signing.VerifyKey(pub_key_bytes)
|
|
70
|
+
|
|
71
|
+
# 3. Verify
|
|
72
|
+
try:
|
|
73
|
+
verify_key.verify(canonical_bytes, sig_bytes)
|
|
74
|
+
return True
|
|
75
|
+
except nacl.exceptions.BadSignatureError:
|
|
76
|
+
return False
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
agentguard/__init__.py,sha256=2tS4Es8Nxq0Cv9qq4kuLuMLfvy4LdkEu-jIGvUD88Gg,1368
|
|
2
|
+
agentguard/auth.py,sha256=fDMc79ObNCcRwd1d0zD5CB2kvW54Acx-rJfT3KSN6Qg,603
|
|
3
|
+
agentguard/client.py,sha256=qyo2ZSsMVDscepCDKkx7-GlN5XxInAR8dgZ5v46eYBI,30610
|
|
4
|
+
agentguard/config.py,sha256=zK7IMox6-CPMtnoUC6bJSh9ZGfIS2Y5vMrdeSIVje7s,1525
|
|
5
|
+
agentguard/consent.py,sha256=jv6EPuSQl29JY-ddI9EmSujtnzwb7lDZPOY7os99ifo,1991
|
|
6
|
+
agentguard/errors.py,sha256=wIFcTLHYq6fIc2ibgsh45Zfq7CSZTl0fkgj1VHYfZuc,4983
|
|
7
|
+
agentguard/observability.py,sha256=2xSySYDtPhtiNUKXH0zQ0N4WG7j3ssQqPtOBDK6V2EQ,1706
|
|
8
|
+
agentguard/types.py,sha256=bk6PRB2mUjYaIV3EjrHl1p4Z404T0cRyNCA1yx9qdMg,1868
|
|
9
|
+
agentguard_crypto/__init__.py,sha256=fhK2gyw9TRWCxr_ax6XUItkbBCmlCi5pXEOLdMRKxDU,44
|
|
10
|
+
agentguard_crypto/canonical.py,sha256=TRz_ZW2JxobIXAkXEDIHR4Jk-alAzl6VSOSWpKwm9ag,1188
|
|
11
|
+
agentguard_crypto/models.py,sha256=bom4lIO43HjOzVgfKvMu4zvmBt-OSNO-wgABJpquL8M,3324
|
|
12
|
+
agentguard_crypto/signing.py,sha256=spJ9Mat59tVD6yPcSXMnX6UajDrpz9Bj0t1AgZD-d30,2596
|
|
13
|
+
agentguard_python_sdk-0.2.3.dist-info/licenses/LICENSE,sha256=Z1Q553BLX5ho8O0gS_1VmndaVnz2NWgKMcH9aoKwqmM,1093
|
|
14
|
+
agentguard_python_sdk-0.2.3.dist-info/METADATA,sha256=pcNR9M2k7rvrKsYhaiRP_Ez1lor5ZNSCBj5V5PnCfr8,3812
|
|
15
|
+
agentguard_python_sdk-0.2.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
16
|
+
agentguard_python_sdk-0.2.3.dist-info/top_level.txt,sha256=8cQ4cluQzmuln1QOqGfNlEpAt3QCW5szoNTbbgwWxFY,29
|
|
17
|
+
agentguard_python_sdk-0.2.3.dist-info/RECORD,,
|
{agentguard_python_sdk-0.2.2.dist-info → agentguard_python_sdk-0.2.3.dist-info}/licenses/LICENSE
RENAMED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 AgentGuard Team
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentGuard Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
agentguard/__init__.py,sha256=Bq3i78uxJs0UX1DzAIsrRyNMfyYZCCim7SgaPZN3mjQ,1311
|
|
2
|
-
agentguard/auth.py,sha256=M5o4zPhzVLeAnWLoFsFwZuN3j6X7qUa7HYR01OtEKxU,623
|
|
3
|
-
agentguard/client.py,sha256=qyo2ZSsMVDscepCDKkx7-GlN5XxInAR8dgZ5v46eYBI,30610
|
|
4
|
-
agentguard/config.py,sha256=uCP6gvfxzhqBUYK221Gu-3s8HVB8rRdgwkkDSlfrOb0,1524
|
|
5
|
-
agentguard/consent.py,sha256=jv6EPuSQl29JY-ddI9EmSujtnzwb7lDZPOY7os99ifo,1991
|
|
6
|
-
agentguard/errors.py,sha256=wIFcTLHYq6fIc2ibgsh45Zfq7CSZTl0fkgj1VHYfZuc,4983
|
|
7
|
-
agentguard/observability.py,sha256=VXBM0PMEZpR11opbNsadHsmFSt1-PMswiJvOxX2qAu4,1758
|
|
8
|
-
agentguard/types.py,sha256=bk6PRB2mUjYaIV3EjrHl1p4Z404T0cRyNCA1yx9qdMg,1868
|
|
9
|
-
agentguard_crypto/__init__.py,sha256=pFPIJqEYazD7Rga1AYrG1tll8Jdy2Bb4bEozMhvnxoU,43
|
|
10
|
-
agentguard_crypto/canonical.py,sha256=pF9phSDWbdeFhjjzVKJP3gaHV3vMhBgX2IY2eEDP4rc,1154
|
|
11
|
-
agentguard_crypto/models.py,sha256=RoIgraUvjQrnCPa1g6hjCupjKKSAWAp34MKXfdD6UhY,3216
|
|
12
|
-
agentguard_crypto/signing.py,sha256=fY4DW3h8_6bvLQtysUd7N3LwzHGa-Cn3p15bsFbWAD4,2520
|
|
13
|
-
agentguard_python_sdk-0.2.2.dist-info/licenses/LICENSE,sha256=3XC94whSikVdbiIS8WX0DvxH2433cG0MLE75USX97wI,1072
|
|
14
|
-
agentguard_python_sdk-0.2.2.dist-info/METADATA,sha256=oGMpIl2BrLGqdk_rgUFqnrjP8OrExMg53vHClc7TXb8,3812
|
|
15
|
-
agentguard_python_sdk-0.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
16
|
-
agentguard_python_sdk-0.2.2.dist-info/top_level.txt,sha256=8cQ4cluQzmuln1QOqGfNlEpAt3QCW5szoNTbbgwWxFY,29
|
|
17
|
-
agentguard_python_sdk-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
{agentguard_python_sdk-0.2.2.dist-info → agentguard_python_sdk-0.2.3.dist-info}/top_level.txt
RENAMED
|
File without changes
|