a2spa 0.1.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.

Potentially problematic release.


This version of a2spa might be problematic. Click here for more details.

@@ -0,0 +1,8 @@
1
+ include sdk/README.md
2
+ recursive-include sdk/a2spa py.typed
3
+ prune tests
4
+ prune routes
5
+ prune templates
6
+ prune static
7
+ prune a2spa_sdk
8
+ prune scripts
a2spa-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: a2spa
3
+ Version: 0.1.0
4
+ Summary: A2SPA payload helper library for AI-agent runtime authorization
5
+ Author: AI Modularity
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://aimodularity.com/A2SPA
8
+ Project-URL: Documentation, https://aimodularity.com/A2SPA/docs
9
+ Project-URL: Repository, https://github.com/gavincapriola/A2SPA
10
+ Keywords: a2spa,ai-agents,authorization,payload-helpers,signed-payloads,runtime-security
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Security
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: cryptography==45.0.5
21
+ Provides-Extra: pq
22
+ Requires-Dist: pqcrypto==0.4.0; extra == "pq"
23
+
24
+ # A2SPA Python Payload Helpers
25
+
26
+ Helper-only Python package for building, hashing, and signing A2SPA runtime authorization payloads.
27
+
28
+ The verifier remains server-side. This package does not authorize actions by itself.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install a2spa
34
+ ```
35
+
36
+ For ML-DSA signing support where the optional wheel is available:
37
+
38
+ ```bash
39
+ pip install "a2spa[pq]"
40
+ ```
41
+
42
+ ## Basic Usage
43
+
44
+ ```python
45
+ from a2spa import build_payload_fields, build_signed_request, policy_input
46
+
47
+ payload = build_payload_fields(
48
+ agent_id="user-123_sender",
49
+ target_agent_id="user-123_receiver",
50
+ input_data=policy_input(action="deliver", workflow_scope="messages:deliver"),
51
+ )
52
+
53
+ request_body = build_signed_request(private_key_text, payload)
54
+ ```
55
+
56
+ Send `request_body` to `POST /api/verify_payload` with your account API key in the `x-api-key` header.
57
+
58
+ ## Helper Functions
59
+
60
+ - `build_payload_fields(...)`
61
+ - `policy_input(...)`
62
+ - `policy_template(...)`
63
+ - `state_continuity_claim(...)`
64
+ - `canonicalize_payload(...)`
65
+ - `compute_payload_hash(...)`
66
+ - `finalize_payload(...)`
67
+ - `sign_payload(...)`
68
+ - `build_signed_request(...)`
69
+ - `build_debug_snapshot(...)`
70
+
71
+ ## Security Boundary
72
+
73
+ Private keys must stay in a secret manager, server environment, or deployment secret. Do not paste private keys into chats, logs, client-side code, or source control.
a2spa-0.1.0/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # A2SPA
2
+
3
+ A2SPA is a runtime authorization layer for AI-agent actions. It verifies signed payloads, enforces agent policy, checks state continuity, and emits execution receipts before work is allowed to proceed.
4
+
5
+ This repository contains the Flask verifier application, dashboard, integration docs, and Python/Node payload helper libraries.
6
+
7
+ ## What A2SPA Verifies
8
+
9
+ - A registered agent signed the payload with an active key.
10
+ - The payload has not been replayed with a reused nonce.
11
+ - The sender and receiver policies allow the requested action, scope, spend, currency, and delegation claim.
12
+ - Optional state continuity still matches the registered state hash, sequence, status, and expiry.
13
+ - The verifier can emit a signed execution receipt for audit trails.
14
+
15
+ ## Public Product Routes
16
+
17
+ - `/A2SPA/` - product homepage.
18
+ - `/A2SPA/docs` - human and AI-assistant integration docs.
19
+ - `/A2SPA/integrations` - integration pack and starter guidance.
20
+ - `/A2SPA/agentspoofed` - agent-spoofing threat page.
21
+ - `/A2SPA/dashboard` - authenticated control plane.
22
+ - `/A2SPA/profile` - authenticated account, balance, API-key metadata, and notification status.
23
+
24
+ ## Payload Helper Boundary
25
+
26
+ Distributed helper packages may build payloads, canonicalize them, sign them, and help construct policy/state-continuity claims. Verifier logic and enforcement decisions stay server-side in the Flask application.
27
+
28
+ ## Local Verification
29
+
30
+ ```bash
31
+ python -m pytest
32
+ ```
33
+
34
+ Use `.env.example` for local configuration shape only. Do not commit real `.env` files, Firebase service accounts, receipt-signing keys, API keys, SMTP passwords, or private agent keys.
35
+
36
+ Production operations guidance belongs in private runbooks, not in this repository.
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "a2spa"
7
+ version = "0.1.0"
8
+ description = "A2SPA payload helper library for AI-agent runtime authorization"
9
+ readme = "sdk/README.md"
10
+ requires-python = ">=3.11"
11
+ license = "LicenseRef-Proprietary"
12
+ authors = [
13
+ { name = "AI Modularity" },
14
+ ]
15
+ keywords = ["a2spa", "ai-agents", "authorization", "payload-helpers", "signed-payloads", "runtime-security"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Security",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "cryptography==45.0.5",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ pq = ["pqcrypto==0.4.0"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://aimodularity.com/A2SPA"
34
+ Documentation = "https://aimodularity.com/A2SPA/docs"
35
+ Repository = "https://github.com/gavincapriola/A2SPA"
36
+
37
+ [tool.setuptools]
38
+ package-dir = {"" = "sdk"}
39
+ packages = ["a2spa"]
40
+
41
+ [tool.setuptools.package-data]
42
+ a2spa = ["py.typed"]
@@ -0,0 +1,50 @@
1
+ # A2SPA Python Payload Helpers
2
+
3
+ Helper-only Python package for building, hashing, and signing A2SPA runtime authorization payloads.
4
+
5
+ The verifier remains server-side. This package does not authorize actions by itself.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install a2spa
11
+ ```
12
+
13
+ For ML-DSA signing support where the optional wheel is available:
14
+
15
+ ```bash
16
+ pip install "a2spa[pq]"
17
+ ```
18
+
19
+ ## Basic Usage
20
+
21
+ ```python
22
+ from a2spa import build_payload_fields, build_signed_request, policy_input
23
+
24
+ payload = build_payload_fields(
25
+ agent_id="user-123_sender",
26
+ target_agent_id="user-123_receiver",
27
+ input_data=policy_input(action="deliver", workflow_scope="messages:deliver"),
28
+ )
29
+
30
+ request_body = build_signed_request(private_key_text, payload)
31
+ ```
32
+
33
+ Send `request_body` to `POST /api/verify_payload` with your account API key in the `x-api-key` header.
34
+
35
+ ## Helper Functions
36
+
37
+ - `build_payload_fields(...)`
38
+ - `policy_input(...)`
39
+ - `policy_template(...)`
40
+ - `state_continuity_claim(...)`
41
+ - `canonicalize_payload(...)`
42
+ - `compute_payload_hash(...)`
43
+ - `finalize_payload(...)`
44
+ - `sign_payload(...)`
45
+ - `build_signed_request(...)`
46
+ - `build_debug_snapshot(...)`
47
+
48
+ ## Security Boundary
49
+
50
+ Private keys must stay in a secret manager, server environment, or deployment secret. Do not paste private keys into chats, logs, client-side code, or source control.
@@ -0,0 +1,313 @@
1
+ import base64
2
+ import copy
3
+ import hashlib
4
+ import json
5
+ import re
6
+ import textwrap
7
+ import uuid
8
+ from datetime import datetime, timezone
9
+ from typing import Any
10
+
11
+ from cryptography.hazmat.primitives import hashes, serialization
12
+ from cryptography.hazmat.primitives.asymmetric import padding
13
+
14
+ try:
15
+ from pqcrypto.sign import ml_dsa_65 as _mldsa
16
+ except Exception: # pragma: no cover - optional wheel availability is platform-specific
17
+ _mldsa = None
18
+
19
+
20
+ CLASSICAL_RSA_SHA256_PROFILE = "a2spa.classical.rsa_pkcs1v15_sha256.v1"
21
+ PQ_MLDSA_PROFILE = "a2spa.pq.mldsa.v1"
22
+ DEFAULT_SIGNATURE_PROFILE = CLASSICAL_RSA_SHA256_PROFILE
23
+ MLDSA_PRIVATE_KEY_LABEL = "A2SPA ML-DSA-65 PRIVATE KEY"
24
+ SIGNABLE_PAYLOAD_FIELDS = (
25
+ "agent_id",
26
+ "target_agent_id",
27
+ "timestamp",
28
+ "nonce",
29
+ "input",
30
+ "output",
31
+ "alert_threshold",
32
+ )
33
+ OPTIONAL_SIGNABLE_PAYLOAD_FIELDS = (
34
+ "crypto_profile",
35
+ "state_continuity",
36
+ )
37
+ DEFAULT_ALERT_THRESHOLD = 10
38
+ EXPECTED_HASH_RE = re.compile(r"expected:\s*([0-9a-f]{64})", re.IGNORECASE)
39
+
40
+
41
+ def _csv(values) -> list[str]:
42
+ if values in (None, ""):
43
+ return []
44
+ if isinstance(values, str):
45
+ values = [values]
46
+ return [str(value).strip() for value in values if str(value).strip()]
47
+
48
+
49
+ def policy_template(template: str, **overrides) -> dict[str, Any]:
50
+ templates = {
51
+ "calendar": {
52
+ "allowed_actions": ["schedule_meeting", "update_event"],
53
+ "allowed_workflow_scopes": ["calendar:write"],
54
+ },
55
+ "email": {
56
+ "allowed_actions": ["send_email", "draft_email"],
57
+ "allowed_workflow_scopes": ["email:send", "email:draft"],
58
+ },
59
+ "payment_reservation": {
60
+ "allowed_actions": ["reserve"],
61
+ "allowed_intents": ["payment_reservation"],
62
+ "allowed_workflow_scopes": ["payments:reserve"],
63
+ "allowed_currencies": ["USD"],
64
+ "max_spend_usd": 100.0,
65
+ "require_state_continuity_for_spend": True,
66
+ },
67
+ "crm_write": {
68
+ "allowed_actions": ["create_contact", "update_contact"],
69
+ "allowed_workflow_scopes": ["crm:write"],
70
+ },
71
+ }
72
+ if template not in templates:
73
+ raise ValueError(f"Unknown policy template: {template}")
74
+ policy = dict(templates[template])
75
+ policy.update({key: value for key, value in overrides.items() if value is not None})
76
+ return policy
77
+
78
+
79
+ def policy_input(
80
+ *,
81
+ action: str | None = None,
82
+ intent: str | None = None,
83
+ workflow_scope: str | None = None,
84
+ workflow_scopes=None,
85
+ amount_usd: str | float | None = None,
86
+ currency: str | None = None,
87
+ on_behalf_of: str | None = None,
88
+ **extra,
89
+ ) -> dict[str, Any]:
90
+ data = dict(extra)
91
+ if action:
92
+ data["action"] = action
93
+ if intent:
94
+ data["intent"] = intent
95
+ scopes = _csv(workflow_scopes)
96
+ if workflow_scope:
97
+ data["workflow_scope"] = workflow_scope
98
+ elif scopes:
99
+ data["workflow_scopes"] = scopes
100
+ if amount_usd is not None:
101
+ data["amount_usd"] = str(amount_usd)
102
+ if currency:
103
+ data["currency"] = currency
104
+ if on_behalf_of:
105
+ data["on_behalf_of"] = on_behalf_of
106
+ return data
107
+
108
+
109
+ def canonicalize_value(value: Any) -> str:
110
+ return json.dumps(value, sort_keys=True)
111
+
112
+
113
+ def canonical_state_hash(state: dict) -> str:
114
+ if not isinstance(state, dict):
115
+ raise ValueError("state must be an object")
116
+ return hashlib.sha256(canonicalize_value(state).encode("utf-8")).hexdigest()
117
+
118
+
119
+ def state_continuity_claim(state_id: str, state: dict, sequence: int, expires_at: str | None = None) -> dict[str, Any]:
120
+ claim = {
121
+ "state_id": state_id,
122
+ "state_hash": canonical_state_hash(state),
123
+ "sequence": int(sequence),
124
+ }
125
+ if expires_at:
126
+ claim["expires_at"] = expires_at
127
+ return claim
128
+
129
+
130
+ def strip_security_fields(payload: dict) -> dict:
131
+ return {key: copy.deepcopy(value) for key, value in payload.items() if key not in {"hash", "signature"}}
132
+
133
+
134
+ def normalize_signable_payload(payload: dict) -> dict:
135
+ normalized = {field: copy.deepcopy(payload[field]) for field in SIGNABLE_PAYLOAD_FIELDS if field in payload}
136
+ normalized.setdefault("alert_threshold", DEFAULT_ALERT_THRESHOLD)
137
+ for field in OPTIONAL_SIGNABLE_PAYLOAD_FIELDS:
138
+ if field in payload and payload[field] is not None:
139
+ normalized[field] = copy.deepcopy(payload[field])
140
+ return normalized
141
+
142
+
143
+ def canonicalize_payload(payload: dict) -> str:
144
+ return json.dumps(normalize_signable_payload(payload), sort_keys=True)
145
+
146
+
147
+ def prepare_signed_bytes(payload: dict) -> bytes:
148
+ return canonicalize_payload(payload).encode("utf-8")
149
+
150
+
151
+ def compute_payload_hash(payload: dict) -> str:
152
+ return hashlib.sha256(prepare_signed_bytes(payload)).hexdigest()
153
+
154
+
155
+ def finalize_payload(payload_before_hash: dict) -> dict:
156
+ finalized = strip_security_fields(payload_before_hash)
157
+ finalized["hash"] = compute_payload_hash(finalized)
158
+ return finalized
159
+
160
+
161
+ def build_request_body(payload: dict, signature: str) -> dict:
162
+ return {
163
+ "payload": copy.deepcopy(payload),
164
+ "signature": signature,
165
+ }
166
+
167
+
168
+ def build_payload_fields(
169
+ *,
170
+ agent_id: str,
171
+ target_agent_id: str,
172
+ input_data: Any,
173
+ output_data: Any = None,
174
+ alert_threshold: int = 10,
175
+ signature_profile: str = DEFAULT_SIGNATURE_PROFILE,
176
+ state_continuity: dict | None = None,
177
+ nonce: str | None = None,
178
+ timestamp: str | None = None,
179
+ ) -> dict[str, Any]:
180
+ payload = {
181
+ "agent_id": agent_id,
182
+ "target_agent_id": target_agent_id,
183
+ "timestamp": timestamp or datetime.now(timezone.utc).isoformat(),
184
+ "nonce": nonce or str(uuid.uuid4()),
185
+ "input": input_data,
186
+ "output": output_data if output_data is not None else {"status": "ready"},
187
+ "alert_threshold": alert_threshold,
188
+ "crypto_profile": signature_profile,
189
+ }
190
+ if state_continuity:
191
+ payload["state_continuity"] = state_continuity
192
+ return payload
193
+
194
+
195
+ def sign_payload_rsa(private_key_pem: str, payload: dict) -> str:
196
+ private_key = serialization.load_pem_private_key(private_key_pem.encode("utf-8"), password=None)
197
+ return private_key.sign(
198
+ prepare_signed_bytes(payload),
199
+ padding.PKCS1v15(),
200
+ hashes.SHA256(),
201
+ ).hex()
202
+
203
+
204
+ def _dearmor_mldsa_private_key(key_text: str) -> bytes:
205
+ if _mldsa is None:
206
+ raise RuntimeError("pqcrypto is required for ML-DSA signing")
207
+ if not isinstance(key_text, str):
208
+ raise ValueError("Invalid ML-DSA private key")
209
+
210
+ begin = f"-----BEGIN {MLDSA_PRIVATE_KEY_LABEL}-----"
211
+ end = f"-----END {MLDSA_PRIVATE_KEY_LABEL}-----"
212
+ normalized = key_text.strip().replace("\r\n", "\n")
213
+ if not normalized.startswith(begin) or not normalized.endswith(end):
214
+ raise ValueError("Invalid ML-DSA private key envelope")
215
+
216
+ body = normalized[len(begin) : -len(end)]
217
+ raw = base64.b64decode("".join(body.split()), validate=True)
218
+ if len(raw) != _mldsa.SECRET_KEY_SIZE:
219
+ raise ValueError("Invalid ML-DSA private key length")
220
+ return raw
221
+
222
+
223
+ def sign_payload_mldsa(private_key_text: str, payload: dict) -> str:
224
+ if _mldsa is None:
225
+ raise RuntimeError("pqcrypto is required for ML-DSA signing")
226
+ return _mldsa.sign(_dearmor_mldsa_private_key(private_key_text), prepare_signed_bytes(payload)).hex()
227
+
228
+
229
+ def sign_payload(private_key_text: str, payload: dict, signature_profile: str | None = None) -> str:
230
+ profile = signature_profile or payload.get("crypto_profile") or CLASSICAL_RSA_SHA256_PROFILE
231
+ if profile == PQ_MLDSA_PROFILE:
232
+ return sign_payload_mldsa(private_key_text, payload)
233
+ if profile == CLASSICAL_RSA_SHA256_PROFILE:
234
+ return sign_payload_rsa(private_key_text, payload)
235
+ raise ValueError(f"Unsupported signature profile for client signing: {profile}")
236
+
237
+
238
+ def build_signed_request(private_key_text: str, payload_before_hash: dict) -> dict[str, Any]:
239
+ payload = finalize_payload(payload_before_hash)
240
+ signature = sign_payload(private_key_text, payload_before_hash, payload_before_hash.get("crypto_profile"))
241
+ return build_request_body(payload, signature)
242
+
243
+
244
+ def extract_expected_hash(error_text: str | None) -> str | None:
245
+ if not error_text:
246
+ return None
247
+ match = EXPECTED_HASH_RE.search(str(error_text))
248
+ return match.group(1) if match else None
249
+
250
+
251
+ def build_debug_snapshot(
252
+ payload_before_hash: dict,
253
+ signature: str | None = None,
254
+ final_payload: dict | None = None,
255
+ server_error_text: str | None = None,
256
+ ) -> dict[str, Any]:
257
+ pre_hash_payload = strip_security_fields(payload_before_hash)
258
+ request_payload = copy.deepcopy(final_payload) if final_payload is not None else finalize_payload(pre_hash_payload)
259
+ canonical_string = canonicalize_payload(pre_hash_payload)
260
+ signed_bytes = canonical_string.encode("utf-8")
261
+ snapshot = {
262
+ "payload_before_hash": pre_hash_payload,
263
+ "canonical_string": canonical_string,
264
+ "canonical_bytes_hex": signed_bytes.hex(),
265
+ "client_computed_hash": hashlib.sha256(signed_bytes).hexdigest(),
266
+ "final_request_body": build_request_body(request_payload, signature) if signature is not None else {"payload": request_payload},
267
+ "server_expected_hash": extract_expected_hash(server_error_text),
268
+ }
269
+ if server_error_text is not None:
270
+ snapshot["server_error"] = server_error_text
271
+ return snapshot
272
+
273
+
274
+ class A2SPAClient:
275
+ def __init__(self, agent_id: str, private_key_text: str):
276
+ self.agent_id = agent_id
277
+ self.private_key_text = private_key_text
278
+
279
+ def build_signed_request(self, target_agent_id: str, input_data: Any, output_data: Any = None, **kwargs):
280
+ payload = build_payload_fields(
281
+ agent_id=self.agent_id,
282
+ target_agent_id=target_agent_id,
283
+ input_data=input_data,
284
+ output_data=output_data,
285
+ **kwargs,
286
+ )
287
+ return build_signed_request(self.private_key_text, payload)
288
+
289
+
290
+ __all__ = [
291
+ "A2SPAClient",
292
+ "CLASSICAL_RSA_SHA256_PROFILE",
293
+ "PQ_MLDSA_PROFILE",
294
+ "build_debug_snapshot",
295
+ "build_payload_fields",
296
+ "build_request_body",
297
+ "build_signed_request",
298
+ "canonical_state_hash",
299
+ "canonicalize_payload",
300
+ "canonicalize_value",
301
+ "compute_payload_hash",
302
+ "extract_expected_hash",
303
+ "finalize_payload",
304
+ "normalize_signable_payload",
305
+ "policy_input",
306
+ "policy_template",
307
+ "prepare_signed_bytes",
308
+ "sign_payload",
309
+ "sign_payload_mldsa",
310
+ "sign_payload_rsa",
311
+ "state_continuity_claim",
312
+ "strip_security_fields",
313
+ ]
@@ -0,0 +1 @@
1
+ partial
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: a2spa
3
+ Version: 0.1.0
4
+ Summary: A2SPA payload helper library for AI-agent runtime authorization
5
+ Author: AI Modularity
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://aimodularity.com/A2SPA
8
+ Project-URL: Documentation, https://aimodularity.com/A2SPA/docs
9
+ Project-URL: Repository, https://github.com/gavincapriola/A2SPA
10
+ Keywords: a2spa,ai-agents,authorization,payload-helpers,signed-payloads,runtime-security
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Security
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: cryptography==45.0.5
21
+ Provides-Extra: pq
22
+ Requires-Dist: pqcrypto==0.4.0; extra == "pq"
23
+
24
+ # A2SPA Python Payload Helpers
25
+
26
+ Helper-only Python package for building, hashing, and signing A2SPA runtime authorization payloads.
27
+
28
+ The verifier remains server-side. This package does not authorize actions by itself.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install a2spa
34
+ ```
35
+
36
+ For ML-DSA signing support where the optional wheel is available:
37
+
38
+ ```bash
39
+ pip install "a2spa[pq]"
40
+ ```
41
+
42
+ ## Basic Usage
43
+
44
+ ```python
45
+ from a2spa import build_payload_fields, build_signed_request, policy_input
46
+
47
+ payload = build_payload_fields(
48
+ agent_id="user-123_sender",
49
+ target_agent_id="user-123_receiver",
50
+ input_data=policy_input(action="deliver", workflow_scope="messages:deliver"),
51
+ )
52
+
53
+ request_body = build_signed_request(private_key_text, payload)
54
+ ```
55
+
56
+ Send `request_body` to `POST /api/verify_payload` with your account API key in the `x-api-key` header.
57
+
58
+ ## Helper Functions
59
+
60
+ - `build_payload_fields(...)`
61
+ - `policy_input(...)`
62
+ - `policy_template(...)`
63
+ - `state_continuity_claim(...)`
64
+ - `canonicalize_payload(...)`
65
+ - `compute_payload_hash(...)`
66
+ - `finalize_payload(...)`
67
+ - `sign_payload(...)`
68
+ - `build_signed_request(...)`
69
+ - `build_debug_snapshot(...)`
70
+
71
+ ## Security Boundary
72
+
73
+ Private keys must stay in a secret manager, server environment, or deployment secret. Do not paste private keys into chats, logs, client-side code, or source control.
@@ -0,0 +1,11 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ sdk/README.md
5
+ sdk/a2spa/__init__.py
6
+ sdk/a2spa/py.typed
7
+ sdk/a2spa.egg-info/PKG-INFO
8
+ sdk/a2spa.egg-info/SOURCES.txt
9
+ sdk/a2spa.egg-info/dependency_links.txt
10
+ sdk/a2spa.egg-info/requires.txt
11
+ sdk/a2spa.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ cryptography==45.0.5
2
+
3
+ [pq]
4
+ pqcrypto==0.4.0
@@ -0,0 +1 @@
1
+ a2spa
a2spa-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+