agentauthoritychain 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentauthoritychain
3
+ Version: 0.5.0
4
+ Summary: Fail-closed AI-agent authority verification, delegated capability enforcement, and signed decision receipts
5
+ License: Apache-2.0
6
+ Project-URL: Homepage, https://agentauthoritychain.com/
7
+ Project-URL: Documentation, https://agentauthoritychain.com/quickstart/
8
+ Project-URL: Repository, https://github.com/AgentAuthorityChain/aac
9
+ Project-URL: Security, https://agentauthoritychain.com/.well-known/security.txt
10
+ Keywords: ai-agents,authority,delegation,authorization,capabilities,mcp,fail-closed
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Security
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: cryptography>=43
18
+ Requires-Dist: truststore>=0.10.4
@@ -0,0 +1,15 @@
1
+ # AAC Python client
2
+
3
+ Requires Python 3.10+. The client fails closed and verifies AAC's Ed25519 decision-receipt signature before returning an exact `permit`.
4
+
5
+ ```powershell
6
+ python -m venv .venv
7
+ .\.venv\Scripts\python.exe -m pip install .
8
+ .\.venv\Scripts\python.exe -m aac_public_test
9
+ ```
10
+
11
+ All eight credential-free AAC/AACP cases must print `PASS`. Then supply the once-shown organization credential through a secret manager or `AAC_API_CREDENTIAL`; never commit it. Use `AACClient.from_env()`, `AACClient.verify(signed_scenario)` or `AACClient.enforce(...)`. Async applications use the API-compatible `AsyncAACClient`; it preserves the same fail-closed receipt verification and supports sync or async protected operations. Private signing keys stay outside AAC.
12
+
13
+ Denial is distinct from an unreliable verification: `AACDenied` means AAC returned a signed denial, while configuration, authentication, transport, protocol and receipt-signature failures use their corresponding `AAC*Error` classes. None of these errors includes the credential or authority proof.
14
+
15
+ The signed-in organization console provides a domain-bound First Permit Kit for generating a local Ed25519 key and completing the first real authenticated permit.
@@ -0,0 +1,133 @@
1
+ """Fail-closed AAC verification client using the declared cryptography dependency."""
2
+ import asyncio
3
+ import json
4
+ import os
5
+ import secrets
6
+ import time
7
+ import urllib.error
8
+ import urllib.request
9
+ import base64
10
+ try:
11
+ import truststore
12
+ truststore.inject_into_ssl()
13
+ except ImportError:
14
+ pass
15
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
16
+
17
+
18
+ class AACError(Exception):
19
+ """Base class that never includes credentials or request bodies in its message."""
20
+
21
+
22
+ class AACConfigurationError(AACError):
23
+ pass
24
+
25
+
26
+ class AACTransportError(AACError):
27
+ pass
28
+
29
+
30
+ class AACAuthenticationError(AACError):
31
+ pass
32
+
33
+
34
+ class AACProtocolError(AACError):
35
+ pass
36
+
37
+
38
+ class AACReceiptVerificationError(AACProtocolError):
39
+ pass
40
+
41
+
42
+ class AACDenied(PermissionError, AACError):
43
+ pass
44
+
45
+
46
+ class AACClient:
47
+ def __init__(self, credential, base_url="https://agentauthoritychain.com", timeout=8):
48
+ if not isinstance(credential, str) or not credential.strip():
49
+ raise AACConfigurationError("AAC credential is required.")
50
+ if not isinstance(base_url, str) or not base_url.startswith("https://"):
51
+ raise AACConfigurationError("AAC base URL must use HTTPS.")
52
+ self.credential = credential
53
+ self.base_url = base_url.rstrip("/")
54
+ self.endpoint = self.base_url + "/api/v1/verify"
55
+ self.key_endpoint = self.base_url + "/.well-known/aac-receipt-key.json"
56
+ self.timeout = timeout
57
+
58
+ @classmethod
59
+ def from_env(cls):
60
+ return cls(
61
+ os.environ.get("AAC_API_CREDENTIAL", ""),
62
+ os.environ.get("AAC_BASE_URL", "https://agentauthoritychain.com"),
63
+ float(os.environ.get("AAC_TIMEOUT_SECONDS", "8")),
64
+ )
65
+
66
+ def verify(self, signed_scenario):
67
+ body = json.dumps({"input_type": "signed", "signed_scenario": signed_scenario}).encode()
68
+ request = urllib.request.Request(self.endpoint, data=body, method="POST", headers={
69
+ "Authorization": f"Bearer {self.credential}", "Content-Type": "application/json",
70
+ "X-AAC-Timestamp": str(int(time.time())), "X-AAC-Nonce": secrets.token_urlsafe(24),
71
+ })
72
+ try:
73
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
74
+ payload = json.load(response)
75
+ except urllib.error.HTTPError as exc:
76
+ if exc.code in (401, 403):
77
+ raise AACAuthenticationError("AAC rejected the client authentication.") from exc
78
+ raise AACTransportError("AAC verification request failed.") from exc
79
+ except OSError as exc:
80
+ raise AACTransportError("AAC verification was unavailable.") from exc
81
+ except (TypeError, ValueError) as exc:
82
+ raise AACProtocolError("AAC returned malformed JSON.") from exc
83
+ if not isinstance(payload, dict) or not isinstance(payload.get("result"), str):
84
+ raise AACProtocolError("AAC returned a malformed decision.")
85
+ receipt = payload.get("decision_receipt") if isinstance(payload, dict) else None
86
+ if not isinstance(receipt, dict):
87
+ raise AACProtocolError("AAC returned no decision receipt.")
88
+ if not self._valid_signature(receipt):
89
+ raise AACReceiptVerificationError("AAC receipt verification failed.")
90
+ if payload["result"] != "valid" or receipt.get("decision") != "permit":
91
+ reason = receipt.get("reason", {})
92
+ code = reason.get("code", "authority_denied") if isinstance(reason, dict) else "authority_denied"
93
+ raise AACDenied(f"AAC denied the operation: {code}.")
94
+ return receipt
95
+
96
+ def _valid_signature(self, receipt):
97
+ try:
98
+ signature = receipt["signature"]
99
+ if (receipt.get("receipt_version") != "aac-decision-receipt/1"
100
+ or receipt.get("verifier") != self.base_url
101
+ or signature.get("verification_key") != self.key_endpoint):
102
+ return False
103
+ with urllib.request.urlopen(self.key_endpoint, timeout=self.timeout) as response:
104
+ if hasattr(response, "geturl") and response.geturl() != self.key_endpoint:
105
+ return False
106
+ key = json.load(response)
107
+ if key["kid"] != signature["kid"] or key["algorithm"] != "Ed25519":
108
+ return False
109
+ decode = lambda value: base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
110
+ unsigned = {name: value for name, value in receipt.items() if name != "signature"}
111
+ canonical = json.dumps(unsigned, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
112
+ Ed25519PublicKey.from_public_bytes(decode(key["public_key"])).verify(decode(signature["value"]), canonical)
113
+ return True
114
+ except Exception:
115
+ return False
116
+
117
+ def enforce(self, signed_scenario, protected_operation, *args, **kwargs):
118
+ receipt = self.verify(signed_scenario)
119
+ return protected_operation(*args, **kwargs), receipt
120
+
121
+
122
+ class AsyncAACClient(AACClient):
123
+ """Async facade over the dependency-minimal sync transport."""
124
+
125
+ async def verify(self, signed_scenario):
126
+ return await asyncio.to_thread(super().verify, signed_scenario)
127
+
128
+ async def enforce(self, signed_scenario, protected_operation, *args, **kwargs):
129
+ receipt = await self.verify(signed_scenario)
130
+ result = protected_operation(*args, **kwargs)
131
+ if hasattr(result, "__await__"):
132
+ result = await result
133
+ return result, receipt
@@ -0,0 +1,59 @@
1
+ """Credential-free AAC/AACP public conformance runner."""
2
+ import json
3
+ import sys
4
+ import urllib.error
5
+ import urllib.request
6
+ from concurrent.futures import ThreadPoolExecutor
7
+
8
+ try:
9
+ import truststore
10
+ truststore.inject_into_ssl()
11
+ except ImportError:
12
+ pass
13
+
14
+
15
+ CASES = (
16
+ ("/api/v1/evaluate?case=valid", {"result": "valid", "decision": "permit"}),
17
+ ("/api/v1/evaluate?case=widened", {"result": "invalid", "decision": "deny"}),
18
+ ("/api/v1/evaluate?case=expired", {"result": "invalid", "decision": "deny"}),
19
+ ("/api/aacp/v1/evaluate?case=valid", {"valid": True}),
20
+ ("/api/aacp/v1/evaluate?case=invalid_signature", {"valid": False}),
21
+ ("/api/aacp/v1/evaluate?case=stale_acceptance", {"valid": False}),
22
+ ("/api/aacp/v1/evaluate?case=broken_event_chain", {"valid": False}),
23
+ ("/api/aacp/v1/evaluate?case=evidence_substitution", {"valid": False}),
24
+ )
25
+
26
+
27
+ def _run_case(base_url, opener, case):
28
+ path, expected = case
29
+ try:
30
+ request = urllib.request.Request(base_url.rstrip("/") + path, headers={"Accept": "application/json", "User-Agent": "AAC-Official-Conformance/1.0"})
31
+ with opener(request, timeout=30) as response:
32
+ payload = json.load(response)
33
+ if response.status != 200:
34
+ raise RuntimeError(f"HTTP {response.status}")
35
+ actual = {"valid": payload.get("valid")} if "valid" in expected else {"result": payload.get("result"), "decision": payload.get("decision_receipt", {}).get("decision")}
36
+ passed = actual == expected
37
+ except (OSError, ValueError, urllib.error.HTTPError, KeyError) as exc:
38
+ reason = getattr(exc, "reason", exc)
39
+ detail = str(reason).encode(sys.stdout.encoding or "utf-8", errors="backslashreplace").decode(sys.stdout.encoding or "utf-8", errors="replace")
40
+ actual, passed = {"error": type(exc).__name__, "detail": detail[:240]}, False
41
+ return {"path": path, "expected": expected, "actual": actual, "passed": passed}
42
+
43
+
44
+ def run(base_url="https://agentauthoritychain.com", opener=urllib.request.urlopen):
45
+ with ThreadPoolExecutor(max_workers=2) as executor:
46
+ return list(executor.map(lambda case: _run_case(base_url, opener, case), CASES))
47
+
48
+
49
+ def main():
50
+ arguments = [item for item in sys.argv[1:] if item != "--direct"]
51
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({})).open if "--direct" in sys.argv[1:] else urllib.request.urlopen
52
+ results = run(arguments[0] if arguments else "https://agentauthoritychain.com", opener=opener)
53
+ for item in results:
54
+ print(("PASS" if item["passed"] else "FAIL"), item["path"], "" if item["passed"] else item["actual"])
55
+ return 0 if all(item["passed"] for item in results) else 1
56
+
57
+
58
+ if __name__ == "__main__":
59
+ raise SystemExit(main())
@@ -0,0 +1,27 @@
1
+ """Independent offline AAC decision-receipt verifier."""
2
+ import base64
3
+ import hashlib
4
+ import json
5
+
6
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
7
+
8
+
9
+ def _decode(value): return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
10
+
11
+
12
+ def verify_receipt(receipt, key_document, request=None, authority_chain=None):
13
+ signature = receipt.get("signature", {})
14
+ if key_document.get("schema") == "aac-receipt-key-set/1":
15
+ matching = [key for key in key_document.get("keys", []) if key.get("kid") == signature.get("kid")]
16
+ if len(matching) != 1: return {"valid": False, "reason": "KEY_OR_ALGORITHM_MISMATCH"}
17
+ key_document = matching[0]
18
+ if signature.get("algorithm") != "Ed25519" or signature.get("kid") != key_document.get("kid"): return {"valid": False, "reason": "KEY_OR_ALGORITHM_MISMATCH"}
19
+ unsigned = {key: value for key, value in receipt.items() if key != "signature"}
20
+ canonical = json.dumps(unsigned, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
21
+ try: Ed25519PublicKey.from_public_bytes(_decode(key_document["public_key"])).verify(_decode(signature["value"]), canonical)
22
+ except Exception: return {"valid": False, "reason": "INVALID_SIGNATURE"}
23
+ if request is not None and receipt.get("request_hash") != "sha256:" + hashlib.sha256(request).hexdigest(): return {"valid": False, "reason": "REQUEST_HASH_MISMATCH"}
24
+ if authority_chain is not None:
25
+ expected = "sha256:" + hashlib.sha256(authority_chain).hexdigest()
26
+ if receipt.get("authority_chain_hash") != expected: return {"valid": False, "reason": "AUTHORITY_CHAIN_HASH_MISMATCH"}
27
+ return {"valid": True, "decision": receipt.get("decision"), "kid": signature["kid"], "request_hash": receipt.get("request_hash"), "authority_chain_hash": receipt.get("authority_chain_hash")}
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentauthoritychain
3
+ Version: 0.5.0
4
+ Summary: Fail-closed AI-agent authority verification, delegated capability enforcement, and signed decision receipts
5
+ License: Apache-2.0
6
+ Project-URL: Homepage, https://agentauthoritychain.com/
7
+ Project-URL: Documentation, https://agentauthoritychain.com/quickstart/
8
+ Project-URL: Repository, https://github.com/AgentAuthorityChain/aac
9
+ Project-URL: Security, https://agentauthoritychain.com/.well-known/security.txt
10
+ Keywords: ai-agents,authority,delegation,authorization,capabilities,mcp,fail-closed
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Security
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: cryptography>=43
18
+ Requires-Dist: truststore>=0.10.4
@@ -0,0 +1,10 @@
1
+ README.md
2
+ aac_client.py
3
+ aac_public_test.py
4
+ aac_receipt_verifier.py
5
+ pyproject.toml
6
+ agentauthoritychain.egg-info/PKG-INFO
7
+ agentauthoritychain.egg-info/SOURCES.txt
8
+ agentauthoritychain.egg-info/dependency_links.txt
9
+ agentauthoritychain.egg-info/requires.txt
10
+ agentauthoritychain.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ cryptography>=43
2
+ truststore>=0.10.4
@@ -0,0 +1,3 @@
1
+ aac_client
2
+ aac_public_test
3
+ aac_receipt_verifier
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agentauthoritychain"
7
+ version = "0.5.0"
8
+ description = "Fail-closed AI-agent authority verification, delegated capability enforcement, and signed decision receipts"
9
+ requires-python = ">=3.10"
10
+ dependencies = ["cryptography>=43", "truststore>=0.10.4"]
11
+ license = {text = "Apache-2.0"}
12
+ keywords = ["ai-agents", "authority", "delegation", "authorization", "capabilities", "mcp", "fail-closed"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Security",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://agentauthoritychain.com/"
23
+ Documentation = "https://agentauthoritychain.com/quickstart/"
24
+ Repository = "https://github.com/AgentAuthorityChain/aac"
25
+ Security = "https://agentauthoritychain.com/.well-known/security.txt"
26
+
27
+ [tool.setuptools]
28
+ py-modules = ["aac_client", "aac_receipt_verifier", "aac_public_test"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+