ai-oriented-a2a 0.3.2__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,51 @@
1
+ """Minimal stdlib library for the AI-oriented A2A peer protocol (v0.3).
2
+
3
+ Required dependencies: none (Python standard library only). Schema
4
+ validation is an optional extra: ``pip install 'ai-oriented-a2a[schema]'``.
5
+
6
+ Signature verification and the replay window stay off unless the
7
+ caller passes ``sign_key`` or ``replay_window``. There is no HTTP
8
+ server and no central component in this package.
9
+ """
10
+
11
+ from .protocol import (
12
+ RESERVED_PACKET_REF,
13
+ SUPPORTED_MAJOR_MINOR,
14
+ VERSION_PATTERN,
15
+ canonical,
16
+ check_incoming,
17
+ check_replay,
18
+ new_id,
19
+ packet_ref_of,
20
+ parse_timestamp,
21
+ reject_result,
22
+ sign_message,
23
+ supported_version,
24
+ verify_message,
25
+ version_rejection,
26
+ )
27
+ from .validate import SchemaDependencyError, SCHEMA_KINDS, schema_dir, validate
28
+
29
+ __version__ = "0.3.2"
30
+
31
+ __all__ = [
32
+ "RESERVED_PACKET_REF",
33
+ "SCHEMA_KINDS",
34
+ "SUPPORTED_MAJOR_MINOR",
35
+ "VERSION_PATTERN",
36
+ "SchemaDependencyError",
37
+ "__version__",
38
+ "canonical",
39
+ "check_incoming",
40
+ "check_replay",
41
+ "new_id",
42
+ "packet_ref_of",
43
+ "parse_timestamp",
44
+ "reject_result",
45
+ "schema_dir",
46
+ "sign_message",
47
+ "supported_version",
48
+ "validate",
49
+ "verify_message",
50
+ "version_rejection",
51
+ ]
@@ -0,0 +1,232 @@
1
+ """Stdlib helpers for the AI-oriented A2A peer protocol (v0.3).
2
+
3
+ No mandatory signature or replay checks, no HTTP server, and no
4
+ central components. Signature and replay stay off unless the caller
5
+ passes ``sign_key`` or ``replay_window``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import hmac
12
+ import json
13
+ import re
14
+ import time
15
+ import uuid
16
+ from datetime import datetime, timezone
17
+ from typing import Any, Dict, Optional, Set, Tuple
18
+
19
+ RESERVED_PACKET_REF = "unknown"
20
+ VERSION_PATTERN = re.compile(r"^\d+\.\d+(\.\d+)?$")
21
+ SUPPORTED_MAJOR_MINOR = (0, 3)
22
+
23
+
24
+ def new_id(prefix: str = "pkt") -> str:
25
+ return f"{prefix}-{uuid.uuid4().hex[:8]}"
26
+
27
+
28
+ def canonical(obj: Dict[str, Any]) -> bytes:
29
+ """Return canonical JSON bytes used for HMAC-SHA256 signatures.
30
+
31
+ The canonical form is the UTF-8 encoding of
32
+ ``json.dumps(obj, separators=(",", ":"), sort_keys=True, ensure_ascii=False)``.
33
+ The caller MUST pass an object that already excludes the ``signature``
34
+ field; other fields (including ``version``, ``nonce``, and ``timestamp``)
35
+ are included. ``sort_keys=True`` applies recursively. Integer Unix
36
+ seconds are recommended for ``timestamp`` so float formatting does not
37
+ affect the digest.
38
+ """
39
+ return json.dumps(
40
+ obj, separators=(",", ":"), sort_keys=True, ensure_ascii=False
41
+ ).encode("utf-8")
42
+
43
+
44
+ def sign_message(msg: Dict[str, Any], key: str) -> Dict[str, Any]:
45
+ body = {k: v for k, v in msg.items() if k != "signature"}
46
+ sig = hmac.new(key.encode(), canonical(body), hashlib.sha256).hexdigest()
47
+ out = dict(msg)
48
+ out["signature"] = {"alg": "hmac-sha256", "value": sig}
49
+ return out
50
+
51
+
52
+ def verify_message(msg: Dict[str, Any], key: str) -> bool:
53
+ sig_obj = msg.get("signature")
54
+ if not isinstance(sig_obj, dict) or sig_obj.get("alg") != "hmac-sha256":
55
+ return False
56
+ body = {k: v for k, v in msg.items() if k != "signature"}
57
+ expected = hmac.new(key.encode(), canonical(body), hashlib.sha256).hexdigest()
58
+ value = sig_obj.get("value", "")
59
+ if not isinstance(value, str):
60
+ return False
61
+ return hmac.compare_digest(expected, value)
62
+
63
+
64
+ def packet_ref_of(msg: Optional[Dict[str, Any]]) -> str:
65
+ """Usable packet_ref for an Absorption Result, or the reserved literal."""
66
+ if not msg:
67
+ return RESERVED_PACKET_REF
68
+ for key in ("id", "packet_ref"):
69
+ val = msg.get(key)
70
+ if isinstance(val, str) and val.strip():
71
+ return val
72
+ return RESERVED_PACKET_REF
73
+
74
+
75
+ def reject_result(msg: Optional[Dict[str, Any]], reason: str) -> Dict[str, Any]:
76
+ return {
77
+ "packet_ref": packet_ref_of(msg),
78
+ "status": "rejected",
79
+ "reason": reason,
80
+ }
81
+
82
+
83
+ def parse_timestamp(ts: Any) -> Optional[float]:
84
+ """Parse a unix-seconds number or an ISO-8601 string into epoch seconds.
85
+
86
+ Naive (no timezone) strings are treated as UTC. This is compatibility
87
+ behaviour only: the published schemas require a timezone, and strict
88
+ receivers MAY reject such strings with ``schema_invalid:``. A trailing
89
+ ``Z`` is accepted. Returns None if the value cannot be parsed.
90
+ """
91
+ if isinstance(ts, bool):
92
+ return None
93
+ if isinstance(ts, (int, float)):
94
+ return float(ts)
95
+ if isinstance(ts, str) and ts.strip():
96
+ text = ts.strip()
97
+ if text.endswith("Z"):
98
+ text = text[:-1] + "+00:00"
99
+ try:
100
+ dt = datetime.fromisoformat(text)
101
+ except ValueError:
102
+ return None
103
+ if dt.tzinfo is None:
104
+ dt = dt.replace(tzinfo=timezone.utc)
105
+ return dt.timestamp()
106
+ return None
107
+
108
+
109
+ def supported_version(value: str) -> bool:
110
+ """True if an on-wire version string is the current 0.3 minor line."""
111
+ if not VERSION_PATTERN.match(value):
112
+ return False
113
+ parts = value.split(".")
114
+ return (int(parts[0]), int(parts[1])) == SUPPORTED_MAJOR_MINOR
115
+
116
+
117
+ def version_rejection(msg: Dict[str, Any]) -> Optional[str]:
118
+ """Return an unsupported_version reason, or None if the message is fine.
119
+
120
+ Omitted version is treated as the current normative version (0.3).
121
+ A leading ``v`` or any other pattern failure, and any recognized-pattern
122
+ value that is not the 0.3 line, MUST be rejected.
123
+ """
124
+ if "version" not in msg:
125
+ return None
126
+ raw = msg["version"]
127
+ display = raw if isinstance(raw, str) else repr(raw)
128
+ if not isinstance(raw, str) or not supported_version(raw):
129
+ return f"unsupported_version: received {display}"
130
+ return None
131
+
132
+
133
+ # Replay cache bounds. ``seen`` stays a plain ``set`` of ``(nonce, str(int(ts)))``
134
+ # keys; when it grows past SEEN_MAX it is trimmed to SEEN_TRIM_TO, dropping
135
+ # expired keys first and then the oldest timestamps (never arbitrary keys).
136
+ SEEN_MAX = 1000
137
+ SEEN_TRIM_TO = 800
138
+
139
+
140
+ def _key_ts(key: Tuple[str, str]) -> float:
141
+ try:
142
+ return float(key[1])
143
+ except (TypeError, ValueError, IndexError):
144
+ return float("-inf")
145
+
146
+
147
+ def _evict(seen: Set[Tuple[str, str]], now: float, window_seconds: float) -> None:
148
+ """Trim ``seen`` to SEEN_TRIM_TO: expired keys first, then oldest timestamp.
149
+
150
+ Expired keys can never be accepted again (the timestamp check runs
151
+ first), so dropping them is free. Dropping a key that is still inside
152
+ the window means that nonce could be replayed; keep the window short
153
+ if that matters (docs/SECURITY.md).
154
+ """
155
+ for key in [k for k in seen if abs(now - _key_ts(k)) > window_seconds]:
156
+ seen.discard(key)
157
+ excess = len(seen) - SEEN_TRIM_TO
158
+ if excess > 0:
159
+ for key in sorted(seen, key=lambda k: (_key_ts(k), k))[:excess]:
160
+ seen.discard(key)
161
+
162
+
163
+ def check_replay(
164
+ msg: Dict[str, Any],
165
+ seen: Set[Tuple[str, str]],
166
+ window_seconds: float,
167
+ ) -> Optional[str]:
168
+ """Return reason string if replay/stale, else None. Mutates seen on accept.
169
+
170
+ Called only when a replay window has been enabled.
171
+ Both nonce and timestamp are required when the window is on.
172
+
173
+ A nonce is a duplicate if it is already in ``seen``, whatever its
174
+ timestamp (the same nonce with a different timestamp is rejected).
175
+ ``seen`` is a plain set of ``(nonce, str(int(timestamp)))`` keys; when it
176
+ exceeds SEEN_MAX it is trimmed to SEEN_TRIM_TO by dropping expired keys
177
+ first and then the oldest timestamps.
178
+ """
179
+ nonce = msg.get("nonce")
180
+ ts = msg.get("timestamp")
181
+ if nonce is None or ts is None:
182
+ return "replay_or_stale: nonce and timestamp required when replay window is enabled"
183
+ if not isinstance(nonce, str) or not nonce:
184
+ return "replay_or_stale: missing or empty nonce"
185
+ parsed = parse_timestamp(ts)
186
+ if parsed is None:
187
+ return "replay_or_stale: missing or unparsable timestamp"
188
+ now = time.time()
189
+ if abs(now - parsed) > window_seconds:
190
+ return "replay_or_stale: timestamp outside window"
191
+ if any(key[0] == nonce for key in seen):
192
+ return "replay_or_stale: duplicate nonce"
193
+ seen.add((nonce, str(int(parsed))))
194
+ if len(seen) > SEEN_MAX:
195
+ _evict(seen, now, window_seconds)
196
+ return None
197
+
198
+
199
+ def check_incoming(
200
+ msg: Dict[str, Any],
201
+ sign_key: Optional[str] = None,
202
+ replay_window: Optional[float] = None,
203
+ seen: Optional[Set[Tuple[str, str]]] = None,
204
+ ) -> Optional[Dict[str, Any]]:
205
+ """Return a rejected Absorption Result if checks fail, else None.
206
+
207
+ Check order is fixed: version first, then signature (only if
208
+ ``sign_key`` is passed), then replay (only if ``replay_window`` is
209
+ passed). Default leaves signature and replay off. When a replay
210
+ window is enabled, both ``nonce`` and ``timestamp`` are required.
211
+ The reserved packet_ref ``"unknown"`` is used for early failures
212
+ that have no usable id; it MUST NOT be used for a successful
213
+ absorption.
214
+
215
+ ``seen`` is the sliding window of ``(nonce, timestamp)`` keys. The
216
+ caller MUST pass and reuse the same set across calls when replay
217
+ is on; if it is omitted, a fresh empty set is used for this call
218
+ only (duplicate detection will not persist).
219
+ """
220
+ version_reason = version_rejection(msg)
221
+ if version_reason:
222
+ return reject_result(msg, version_reason)
223
+ if sign_key:
224
+ if not verify_message(msg, sign_key):
225
+ return reject_result(msg, "signature verification failed")
226
+ if replay_window is not None:
227
+ if seen is None:
228
+ seen = set()
229
+ replay_reason = check_replay(msg, seen, replay_window)
230
+ if replay_reason:
231
+ return reject_result(msg, replay_reason)
232
+ return None
File without changes
@@ -0,0 +1,59 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ai-oriented-a2a.local/schema/absorption-result.schema.json",
4
+ "title": "AbsorptionResult",
5
+ "type": "object",
6
+ "required": ["packet_ref", "status"],
7
+ "properties": {
8
+ "packet_ref": { "type": "string", "minLength": 1 },
9
+ "status": { "enum": ["absorbed", "rejected", "isolated"] },
10
+ "reason": { "type": "string", "minLength": 1, "pattern": ".*\\S.*" },
11
+ "effect_summary": {
12
+ "type": "object",
13
+ "required": ["note"],
14
+ "properties": {
15
+ "note": { "type": "string", "minLength": 1, "pattern": ".*\\S.*" },
16
+ "metrics": {
17
+ "type": "object",
18
+ "additionalProperties": {
19
+ "type": ["number", "string", "boolean"]
20
+ }
21
+ }
22
+ },
23
+ "additionalProperties": false
24
+ },
25
+ "version": {
26
+ "type": "string",
27
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?$",
28
+ "description": "Optional on-wire protocol version. Canonical value is \"0.3\". Values with a leading v (e.g. \"v0.3\") are treated as unknown and MUST be rejected."
29
+ },
30
+ "nonce": { "type": "string", "minLength": 1 },
31
+ "timestamp": { "oneOf": [{ "type": "number" }, { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]{3}|\\.[0-9]{6})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$" }] },
32
+ "signature": {
33
+ "type": "object",
34
+ "required": ["alg", "value"],
35
+ "properties": {
36
+ "alg": { "type": "string", "minLength": 1 },
37
+ "value": { "type": "string", "minLength": 1 }
38
+ },
39
+ "additionalProperties": false
40
+ }
41
+ },
42
+ "allOf": [
43
+ {
44
+ "if": {
45
+ "properties": { "status": { "const": "absorbed" } },
46
+ "required": ["status"]
47
+ },
48
+ "then": { "required": ["effect_summary"] }
49
+ },
50
+ {
51
+ "if": {
52
+ "properties": { "status": { "enum": ["rejected", "isolated"] } },
53
+ "required": ["status"]
54
+ },
55
+ "then": { "required": ["reason"] }
56
+ }
57
+ ],
58
+ "additionalProperties": false
59
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ai-oriented-a2a.local/schema/agent-profile.schema.json",
4
+ "title": "AgentProfile",
5
+ "type": "object",
6
+ "required": ["constraints", "improvement_prefs", "shareable_types"],
7
+ "properties": {
8
+ "constraints": { "type": "array", "items": { "type": "string" } },
9
+ "improvement_prefs": { "type": "array", "items": { "type": "string" } },
10
+ "shareable_types": { "type": "array", "items": { "type": "string" } },
11
+ "interest_tags": {
12
+ "type": "array",
13
+ "items": { "type": "string" },
14
+ "description": "Optional light broadcast interest tags for discovery; peers MAY ignore unknown tags."
15
+ },
16
+ "version": {
17
+ "type": "string",
18
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?$",
19
+ "description": "Optional on-wire protocol version. Canonical value is \"0.3\". Values with a leading v (e.g. \"v0.3\") are treated as unknown and MUST be rejected."
20
+ },
21
+ "nonce": { "type": "string", "minLength": 1 },
22
+ "timestamp": { "oneOf": [{ "type": "number" }, { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]{3}|\\.[0-9]{6})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$" }] },
23
+ "signature": {
24
+ "type": "object",
25
+ "required": ["alg", "value"],
26
+ "properties": {
27
+ "alg": { "type": "string", "minLength": 1 },
28
+ "value": { "type": "string", "minLength": 1 }
29
+ },
30
+ "additionalProperties": false
31
+ },
32
+ "identity": {
33
+ "type": "object",
34
+ "required": ["fingerprint"],
35
+ "properties": {
36
+ "fingerprint": { "type": "string", "minLength": 1 },
37
+ "alg": { "type": "string" }
38
+ },
39
+ "additionalProperties": false,
40
+ "description": "Optional identity binding (public-key fingerprint or opaque identifier). When present, fingerprint is required."
41
+ }
42
+ },
43
+ "additionalProperties": false
44
+ }
@@ -0,0 +1,80 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ai-oriented-a2a.local/schema/experience-packet.schema.json",
4
+ "title": "ExperiencePacket",
5
+ "type": "object",
6
+ "required": ["id", "type", "content", "absorption"],
7
+ "properties": {
8
+ "id": { "type": "string", "minLength": 1 },
9
+ "type": { "enum": ["failure_mode", "strategy_fragment", "tool_trajectory", "other"] },
10
+ "content": { "type": "object" },
11
+ "absorption": {
12
+ "type": "object",
13
+ "required": ["method"],
14
+ "properties": {
15
+ "method": {
16
+ "enum": [
17
+ "prompt_injection",
18
+ "retrieval",
19
+ "local_adapter_hint",
20
+ "trajectory_replay",
21
+ "parameter_delta",
22
+ "other"
23
+ ],
24
+ "description": "MUST support prompt_injection and retrieval. OPTIONAL: local_adapter_hint, trajectory_replay. RESERVED/optional: parameter_delta (receivers without support MUST reject)."
25
+ },
26
+ "target": { "type": "string" },
27
+ "hint": { "type": "string" }
28
+ },
29
+ "additionalProperties": false
30
+ },
31
+ "expiry": { "type": "object" },
32
+ "prior_outcomes": {
33
+ "type": "object",
34
+ "properties": {
35
+ "absorbed_count": { "type": "integer", "minimum": 0 },
36
+ "rejected_count": { "type": "integer", "minimum": 0 },
37
+ "last_status": { "enum": ["absorbed", "rejected", "isolated", "none"] }
38
+ },
39
+ "additionalProperties": false
40
+ },
41
+ "provenance": {
42
+ "type": "object",
43
+ "required": ["source_ref"],
44
+ "properties": {
45
+ "source_ref": { "type": "string", "minLength": 1 },
46
+ "chain": {
47
+ "type": "array",
48
+ "items": { "type": "string" }
49
+ }
50
+ },
51
+ "additionalProperties": false
52
+ },
53
+ "verification_claim": {
54
+ "type": "object",
55
+ "required": ["expected_effect"],
56
+ "properties": {
57
+ "expected_effect": { "type": "string", "minLength": 1 },
58
+ "replay_hint": { "type": "string" }
59
+ },
60
+ "additionalProperties": false
61
+ },
62
+ "version": {
63
+ "type": "string",
64
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?$",
65
+ "description": "Optional on-wire protocol version. Canonical value is \"0.3\". Values with a leading v (e.g. \"v0.3\") are treated as unknown and MUST be rejected."
66
+ },
67
+ "nonce": { "type": "string", "minLength": 1 },
68
+ "timestamp": { "oneOf": [{ "type": "number" }, { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]{3}|\\.[0-9]{6})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$" }] },
69
+ "signature": {
70
+ "type": "object",
71
+ "required": ["alg", "value"],
72
+ "properties": {
73
+ "alg": { "type": "string", "minLength": 1 },
74
+ "value": { "type": "string", "minLength": 1 }
75
+ },
76
+ "additionalProperties": false
77
+ }
78
+ },
79
+ "additionalProperties": false
80
+ }
@@ -0,0 +1,118 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ai-oriented-a2a.local/schema/negotiation.schema.json",
4
+ "title": "NegotiationMessage",
5
+ "oneOf": [
6
+ {
7
+ "type": "object",
8
+ "required": ["proposal_id", "target_constraint", "proposed_change"],
9
+ "properties": {
10
+ "proposal_id": { "type": "string", "minLength": 1 },
11
+ "target_constraint": { "type": "string" },
12
+ "proposed_change": { "type": "string" },
13
+ "offered_packet": { "$ref": "experience-packet.schema.json" },
14
+ "conditional_packet": { "$ref": "experience-packet.schema.json" },
15
+ "constraint_scope": { "type": "string" },
16
+ "duration": { "type": "string" },
17
+ "version": {
18
+ "type": "string",
19
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?$",
20
+ "description": "Optional on-wire protocol version. Canonical value is \"0.3\". Values with a leading v (e.g. \"v0.3\") are treated as unknown and MUST be rejected."
21
+ },
22
+ "nonce": { "type": "string", "minLength": 1 },
23
+ "timestamp": { "oneOf": [{ "type": "number" }, { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]{3}|\\.[0-9]{6})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$" }] },
24
+ "signature": {
25
+ "type": "object",
26
+ "required": ["alg", "value"],
27
+ "properties": {
28
+ "alg": { "type": "string", "minLength": 1 },
29
+ "value": { "type": "string", "minLength": 1 }
30
+ },
31
+ "additionalProperties": false
32
+ }
33
+ },
34
+ "additionalProperties": false
35
+ },
36
+ {
37
+ "type": "object",
38
+ "required": ["proposal_id"],
39
+ "properties": {
40
+ "proposal_id": { "type": "string" },
41
+ "status": { "const": "accept" },
42
+ "version": {
43
+ "type": "string",
44
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?$",
45
+ "description": "Optional on-wire protocol version. Canonical value is \"0.3\". Values with a leading v (e.g. \"v0.3\") are treated as unknown and MUST be rejected."
46
+ },
47
+ "nonce": { "type": "string", "minLength": 1 },
48
+ "timestamp": { "oneOf": [{ "type": "number" }, { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]{3}|\\.[0-9]{6})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$" }] },
49
+ "signature": {
50
+ "type": "object",
51
+ "required": ["alg", "value"],
52
+ "properties": {
53
+ "alg": { "type": "string", "minLength": 1 },
54
+ "value": { "type": "string", "minLength": 1 }
55
+ },
56
+ "additionalProperties": false
57
+ }
58
+ },
59
+ "additionalProperties": false
60
+ },
61
+ {
62
+ "type": "object",
63
+ "required": ["proposal_id"],
64
+ "properties": {
65
+ "proposal_id": { "type": "string" },
66
+ "status": { "const": "reject" },
67
+ "reason": { "type": "string" },
68
+ "version": {
69
+ "type": "string",
70
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?$",
71
+ "description": "Optional on-wire protocol version. Canonical value is \"0.3\". Values with a leading v (e.g. \"v0.3\") are treated as unknown and MUST be rejected."
72
+ },
73
+ "nonce": { "type": "string", "minLength": 1 },
74
+ "timestamp": { "oneOf": [{ "type": "number" }, { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]{3}|\\.[0-9]{6})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$" }] },
75
+ "signature": {
76
+ "type": "object",
77
+ "required": ["alg", "value"],
78
+ "properties": {
79
+ "alg": { "type": "string", "minLength": 1 },
80
+ "value": { "type": "string", "minLength": 1 }
81
+ },
82
+ "additionalProperties": false
83
+ }
84
+ },
85
+ "additionalProperties": false
86
+ },
87
+ {
88
+ "type": "object",
89
+ "required": ["proposal_id", "revised_change"],
90
+ "properties": {
91
+ "proposal_id": { "type": "string" },
92
+ "status": { "const": "counter" },
93
+ "revised_change": { "type": "string" },
94
+ "offered_packet": { "$ref": "experience-packet.schema.json" },
95
+ "conditional_packet": { "$ref": "experience-packet.schema.json" },
96
+ "constraint_scope": { "type": "string" },
97
+ "duration": { "type": "string" },
98
+ "version": {
99
+ "type": "string",
100
+ "pattern": "^\\d+\\.\\d+(\\.\\d+)?$",
101
+ "description": "Optional on-wire protocol version. Canonical value is \"0.3\". Values with a leading v (e.g. \"v0.3\") are treated as unknown and MUST be rejected."
102
+ },
103
+ "nonce": { "type": "string", "minLength": 1 },
104
+ "timestamp": { "oneOf": [{ "type": "number" }, { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]{3}|\\.[0-9]{6})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$" }] },
105
+ "signature": {
106
+ "type": "object",
107
+ "required": ["alg", "value"],
108
+ "properties": {
109
+ "alg": { "type": "string", "minLength": 1 },
110
+ "value": { "type": "string", "minLength": 1 }
111
+ },
112
+ "additionalProperties": false
113
+ }
114
+ },
115
+ "additionalProperties": false
116
+ }
117
+ ]
118
+ }
@@ -0,0 +1,273 @@
1
+ """Minimal HTTP binding for AI-oriented A2A v0.3 (standard library only).
2
+
3
+ Import it explicitly; it is not re-exported from the top-level package, so
4
+ ``import ai_oriented_a2a`` does not load ``http.server`` or ``urllib``::
5
+
6
+ from ai_oriented_a2a.transport import send_message, make_server, serve
7
+
8
+ Client: ``send_message(url, message, *, timeout=30.0, sign_key=None,
9
+ add_replay_fields=False)`` POSTs one message to ``url + "/a2a/message"`` and
10
+ returns the peer's JSON reply.
11
+
12
+ Server: subclass ``A2ARequestHandler`` and override ``handle_message``, or
13
+ pass a callback to ``make_handler`` / ``make_server`` / ``serve``. The
14
+ handler does everything docs/TRANSPORT.md requires before your code runs:
15
+ 404 for other paths, HTTP 400 + a legal Absorption Result for invalid JSON,
16
+ then ``check_incoming`` (version first, optional signature and replay
17
+ window). Your callback only sees messages that passed. Its result is passed
18
+ through the hard gate (``guard_result``) before it is sent. If the callback
19
+ raises, the peer gets HTTP 500 and a legal rejected Absorption Result with
20
+ the reason ``internal_error: message handler failed`` (no exception text).
21
+
22
+ The package does not decide absorption: the host's callback does.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import logging
29
+ import time
30
+ import urllib.error
31
+ import urllib.request
32
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
33
+ from typing import Any, Callable, Dict, Optional, Set, Tuple
34
+
35
+ from .protocol import (
36
+ RESERVED_PACKET_REF,
37
+ check_incoming,
38
+ new_id,
39
+ reject_result,
40
+ sign_message,
41
+ )
42
+
43
+ ENDPOINT = "/a2a/message"
44
+ DEFAULT_TIMEOUT = 30.0 # docs/TRANSPORT.md: clients SHOULD use >= 30 s
45
+ INTERNAL_ERROR_REASON = "internal_error: message handler failed"
46
+ INVALID_JSON_REASON = "invalid json"
47
+
48
+ Message = Dict[str, Any]
49
+ MessageHandler = Callable[[Message], Message]
50
+
51
+ log = logging.getLogger(__name__)
52
+
53
+
54
+ class TransportError(Exception):
55
+ """Network failure, timeout, or a reply body that is not a JSON object."""
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Client
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ def send_message(
64
+ url: str,
65
+ message: Message,
66
+ *,
67
+ timeout: float = DEFAULT_TIMEOUT,
68
+ sign_key: Optional[str] = None,
69
+ add_replay_fields: bool = False,
70
+ ) -> Message:
71
+ """POST ``message`` to ``url`` + ``/a2a/message`` and return the JSON reply.
72
+
73
+ ``add_replay_fields`` attaches a fresh ``nonce`` and the current integer
74
+ ``timestamp`` (needed when the peer has a replay window enabled).
75
+ ``sign_key`` signs the message last (HMAC-SHA256 over canonical JSON).
76
+
77
+ An HTTP error status whose body is a JSON object (for example the 400
78
+ ``invalid json`` Absorption Result) is returned like a normal reply.
79
+ Raises TransportError on network errors, timeouts, or a non-JSON body.
80
+ The reply is not validated; a peer's ``rejected`` result is a normal
81
+ return value.
82
+ """
83
+ if add_replay_fields:
84
+ message = dict(message)
85
+ message["nonce"] = new_id("nonce")
86
+ message["timestamp"] = int(time.time())
87
+ if sign_key:
88
+ message = sign_message(message, sign_key)
89
+ data = json.dumps(message).encode("utf-8")
90
+ request = urllib.request.Request(
91
+ url.rstrip("/") + ENDPOINT,
92
+ data=data,
93
+ headers={"Content-Type": "application/json"},
94
+ method="POST",
95
+ )
96
+ try:
97
+ with urllib.request.urlopen(request, timeout=timeout) as resp:
98
+ raw = resp.read()
99
+ except urllib.error.HTTPError as exc:
100
+ raw = exc.read()
101
+ except (urllib.error.URLError, OSError) as exc:
102
+ raise TransportError(f"cannot reach {url}: {exc}") from exc
103
+ try:
104
+ body = json.loads(raw.decode("utf-8"))
105
+ except (UnicodeDecodeError, ValueError) as exc:
106
+ raise TransportError("reply body is not JSON") from exc
107
+ if not isinstance(body, dict):
108
+ raise TransportError("reply body is not a JSON object")
109
+ return body
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Server
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ def guard_result(result: Message) -> Message:
118
+ """Apply the SPEC §3.1 hard gate to a reply before it is sent.
119
+
120
+ ``status=absorbed`` without a non-empty ``effect_summary.note``, or with
121
+ the reserved ``packet_ref`` ``unknown``, becomes a rejected result.
122
+ Anything else (other statuses, acks) is returned unchanged.
123
+ """
124
+ if not isinstance(result, dict) or result.get("status") != "absorbed":
125
+ return result
126
+ if result.get("packet_ref") == RESERVED_PACKET_REF:
127
+ return reject_result(
128
+ result, "reserved packet_ref 'unknown' cannot be used for successful absorption"
129
+ )
130
+ summary = result.get("effect_summary")
131
+ note = summary.get("note") if isinstance(summary, dict) else None
132
+ if not (isinstance(note, str) and note.strip()):
133
+ return reject_result(
134
+ result, "hard-gate violation: absorbed result missing non-empty effect_summary.note"
135
+ )
136
+ return result
137
+
138
+
139
+ class A2ARequestHandler(BaseHTTPRequestHandler):
140
+ """``POST /a2a/message`` handler. Override ``handle_message``.
141
+
142
+ Configuration is read from class attributes so a subclass (or
143
+ ``make_handler``) can set it: ``sign_key`` enables signature
144
+ verification and signed replies, ``replay_window`` (seconds) enables the
145
+ replay window, and ``seen_nonces`` is the shared replay set.
146
+ """
147
+
148
+ sign_key: Optional[str] = None
149
+ replay_window: Optional[float] = None
150
+ seen_nonces: Set[Tuple[str, str]] = set()
151
+
152
+ def handle_message(self, msg: Message) -> Message:
153
+ """Return the reply for a message that passed ``check_incoming``."""
154
+ raise NotImplementedError("override handle_message or use make_handler(on_message)")
155
+
156
+ def do_POST(self) -> None: # noqa: N802 (http.server API)
157
+ if self.path != ENDPOINT:
158
+ self.send_error(404)
159
+ return
160
+ length = int(self.headers.get("Content-Length", 0) or 0)
161
+ body = self.rfile.read(length)
162
+ try:
163
+ msg = json.loads(body.decode())
164
+ if not isinstance(msg, dict):
165
+ raise ValueError("message must be a JSON object")
166
+ except Exception:
167
+ self._reply(reject_result(None, INVALID_JSON_REASON), status=400)
168
+ return
169
+
170
+ rejected = check_incoming(
171
+ msg,
172
+ sign_key=self.sign_key,
173
+ replay_window=self.replay_window,
174
+ seen=self.seen_nonces,
175
+ )
176
+ if rejected:
177
+ self._reply(rejected)
178
+ return
179
+
180
+ try:
181
+ reply = guard_result(self.handle_message(msg))
182
+ except Exception:
183
+ # Log locally; never send exception text, paths, or tracebacks.
184
+ log.exception("A2A message handler failed")
185
+ self._reply(reject_result(msg, INTERNAL_ERROR_REASON), status=500)
186
+ return
187
+ self._reply(reply)
188
+
189
+ def _reply(self, payload: Message, status: int = 200) -> None:
190
+ if self.sign_key:
191
+ payload = sign_message(payload, self.sign_key)
192
+ data = json.dumps(payload).encode()
193
+ self.send_response(status)
194
+ self.send_header("Content-Type", "application/json")
195
+ self.send_header("Content-Length", str(len(data)))
196
+ self.end_headers()
197
+ self.wfile.write(data)
198
+
199
+
200
+ def make_handler(
201
+ on_message: MessageHandler,
202
+ *,
203
+ sign_key: Optional[str] = None,
204
+ replay_window: Optional[float] = None,
205
+ ) -> type:
206
+ """Return an ``A2ARequestHandler`` subclass that calls ``on_message``.
207
+
208
+ Each returned class has its own replay set.
209
+ """
210
+ callback = on_message
211
+
212
+ class _Handler(A2ARequestHandler):
213
+ def handle_message(self, msg: Message) -> Message:
214
+ return callback(msg)
215
+
216
+ def log_message(self, format: str, *args: Any) -> None: # noqa: A002
217
+ log.debug("%s - %s", self.address_string(), format % args)
218
+
219
+ _Handler.sign_key = sign_key
220
+ _Handler.replay_window = replay_window
221
+ _Handler.seen_nonces = set()
222
+ return _Handler
223
+
224
+
225
+ def make_server(
226
+ on_message: MessageHandler,
227
+ port: int = 0,
228
+ host: str = "127.0.0.1",
229
+ *,
230
+ sign_key: Optional[str] = None,
231
+ replay_window: Optional[float] = None,
232
+ ) -> ThreadingHTTPServer:
233
+ """Build (but do not start) a threaded server; ``port=0`` picks a free port.
234
+
235
+ Start it with ``server.serve_forever()`` (for example in a thread) and
236
+ stop it with ``server.shutdown()`` and ``server.server_close()``.
237
+ """
238
+ handler = make_handler(on_message, sign_key=sign_key, replay_window=replay_window)
239
+ server = ThreadingHTTPServer((host, port), handler)
240
+ server.daemon_threads = True
241
+ return server
242
+
243
+
244
+ def serve(
245
+ on_message: MessageHandler,
246
+ port: int,
247
+ host: str = "127.0.0.1",
248
+ *,
249
+ sign_key: Optional[str] = None,
250
+ replay_window: Optional[float] = None,
251
+ ) -> None:
252
+ """Serve ``POST /a2a/message`` until interrupted (blocks)."""
253
+ server = make_server(on_message, port, host, sign_key=sign_key, replay_window=replay_window)
254
+ try:
255
+ server.serve_forever()
256
+ except KeyboardInterrupt:
257
+ pass
258
+ finally:
259
+ server.server_close()
260
+
261
+
262
+ __all__ = [
263
+ "A2ARequestHandler",
264
+ "DEFAULT_TIMEOUT",
265
+ "ENDPOINT",
266
+ "INTERNAL_ERROR_REASON",
267
+ "TransportError",
268
+ "guard_result",
269
+ "make_handler",
270
+ "make_server",
271
+ "send_message",
272
+ "serve",
273
+ ]
@@ -0,0 +1,93 @@
1
+ """Optional JSON Schema validation. Requires the ``schema`` extra."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any, Mapping
8
+
9
+ SCHEMA_KINDS = {
10
+ "absorption-result": "absorption-result.schema.json",
11
+ "agent-profile": "agent-profile.schema.json",
12
+ "experience-packet": "experience-packet.schema.json",
13
+ "negotiation": "negotiation.schema.json",
14
+ }
15
+
16
+
17
+ class SchemaDependencyError(ImportError):
18
+ """Raised when validate() is called but jsonschema is not installed."""
19
+
20
+
21
+ def schema_dir() -> Path:
22
+ """Return the directory that holds the published ``*.schema.json`` files.
23
+
24
+ Prefers package data (a built wheel or ``pip install .``). Falls back to
25
+ the repository's top-level ``schema/`` directory for an editable or
26
+ source checkout where the files were not copied into the package.
27
+ """
28
+ here = Path(__file__).resolve().parent
29
+ bundled = here / "schema"
30
+ if bundled.is_dir() and any(bundled.glob("*.schema.json")):
31
+ return bundled
32
+ repo = here.parent / "schema"
33
+ if repo.is_dir() and any(repo.glob("*.schema.json")):
34
+ return repo
35
+ raise FileNotFoundError(
36
+ "Could not find schema JSON files (looked in package data and "
37
+ "the repository schema/ directory)."
38
+ )
39
+
40
+
41
+ def validate(msg: Mapping[str, Any], kind: str) -> None:
42
+ """Validate ``msg`` against a published schema.
43
+
44
+ ``kind`` is one of: absorption-result, agent-profile,
45
+ experience-packet, negotiation.
46
+
47
+ This function performs full schema validation only. It does not
48
+ implement the protocol version check, and its behaviour is
49
+ unchanged: an unknown version such as ``"v0.3"`` fails the schema
50
+ ``version`` pattern and raises ``jsonschema.ValidationError``. It
51
+ does not return a rejected Absorption Result.
52
+
53
+ Receivers MUST call ``check_incoming`` (or ``version_rejection``)
54
+ first and, on an unknown version, return that rejected Absorption
55
+ Result (``status=rejected``, ``reason`` containing
56
+ ``unsupported_version``). Call ``validate`` only for messages that
57
+ have already passed the version check. See docs/TRANSPORT.md
58
+ ("Version check before schema validation").
59
+
60
+ Requires ``pip install 'ai-oriented-a2a[schema]'``. Raises
61
+ ``SchemaDependencyError`` if jsonschema is not installed,
62
+ ``ValueError`` for an unknown kind, and
63
+ ``jsonschema.ValidationError`` if the message is invalid.
64
+ """
65
+ try:
66
+ from jsonschema import Draft202012Validator
67
+ from referencing import Registry, Resource
68
+ except ImportError as exc:
69
+ raise SchemaDependencyError(
70
+ "Schema validation requires the optional extra: "
71
+ "pip install 'ai-oriented-a2a[schema]'"
72
+ ) from exc
73
+
74
+ if kind not in SCHEMA_KINDS:
75
+ raise ValueError(
76
+ f"Unknown schema kind {kind!r}. Expected one of: "
77
+ f"{', '.join(sorted(SCHEMA_KINDS))}"
78
+ )
79
+
80
+ directory = schema_dir()
81
+ resources = []
82
+ for path in directory.glob("*.schema.json"):
83
+ contents = json.loads(path.read_text(encoding="utf-8"))
84
+ resource = Resource.from_contents(contents)
85
+ resources.append((path.name, resource))
86
+ schema_id = contents.get("$id")
87
+ if schema_id:
88
+ resources.append((schema_id, resource))
89
+ registry = Registry().with_resources(resources)
90
+
91
+ schema_path = directory / SCHEMA_KINDS[kind]
92
+ schema = json.loads(schema_path.read_text(encoding="utf-8"))
93
+ Draft202012Validator(schema, registry=registry).validate(msg)
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.5
2
+ Name: ai-oriented-a2a
3
+ Version: 0.3.2
4
+ Summary: Minimal stdlib helpers for the AI-oriented A2A peer protocol (Experience Packets, optional HMAC and replay checks).
5
+ Project-URL: Homepage, https://github.com/robotmarvin0706/ai-oriented-a2a
6
+ Project-URL: Repository, https://github.com/robotmarvin0706/ai-oriented-a2a
7
+ Project-URL: Changelog, https://github.com/robotmarvin0706/ai-oriented-a2a/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/robotmarvin0706/ai-oriented-a2a/issues
9
+ Project-URL: Release, https://github.com/robotmarvin0706/ai-oriented-a2a/releases/tag/v0.3.2
10
+ Author: robotmarvin0706
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: a2a,absorption,experience-packet,peer-protocol
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.8
25
+ Provides-Extra: mcp
26
+ Requires-Dist: mcp<3,>=2; (python_version >= '3.10') and extra == 'mcp'
27
+ Provides-Extra: schema
28
+ Requires-Dist: jsonschema>=4.18; extra == 'schema'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # AI-Oriented A2A (v0.3)
32
+
33
+ Minimal **peer** protocol for AI capability growth: Experience Packets + constraint negotiation.
34
+ Not the Google A2A task/agent-card stack — this repo is about sharing compressed improvement signals between peers.
35
+ - Spec: [SPEC.md](SPEC.md) (v0.3) · Last tagged release: [v0.3.1](https://github.com/robotmarvin0706/ai-oriented-a2a/releases/tag/v0.3.1) · Changelog: [CHANGELOG.md](CHANGELOG.md)
36
+ - Library version on `main`: 0.3.2 (protocol v0.3). Tag `v0.3.2` is created after this version is merged, so v0.3.1 stays the last tagged release until then.
37
+ - Keywords: experience packet, absorption, proposal_id, peer negotiation, trajectory_replay
38
+
39
+ Protocol for AI-to-AI capability growth: first-class Experience Packet exchange, constraint negotiation, proposal_id, no mandatory human mediation or central registry. See SPEC.md and examples/experience-packet.json.
40
+
41
+ Minimal peer protocol for AI capability growth via selective experience exchange and direct constraint negotiation.
42
+
43
+ ## Changes from v0.1
44
+
45
+ - Absorption method layering: MUST (`prompt_injection`, `retrieval`), OPTIONAL (`local_adapter_hint`, `trajectory_replay`), RESERVED (`parameter_delta` with verify+rollback if declared).
46
+ - OPTIONAL Experience Packet fields: `provenance`, `verification_claim`.
47
+ - OPTIONAL Absorption Result field in v0.2: `effect_summary`. In v0.3 this is MUST when `status=absorbed` (non-empty `note`); it remains OPTIONAL for `rejected` and `isolated`.
48
+ - OPTIONAL negotiation propose/counter fields: `conditional_packet`, `constraint_scope`, `duration`.
49
+ - Discovery prose for light interest tags and optional peer metadata forwarding (no new mandatory messages).
50
+ - OPTIONAL Agent Profile `interest_tags`.
51
+ - Compatibility: v0.2 preferred zero hard breaks and kept new fields optional. v0.3 is a deliberate break: `status=absorbed` without `effect_summary.note` is no longer valid. See [docs/VERSIONING.md](docs/VERSIONING.md).
52
+
53
+ ### v0.2 subset note
54
+
55
+ v0.1 parsers still parse core messages (Profile, Experience Packet, negotiation four types, Absorption Result) but cannot use the new primitives (`provenance`, `verification_claim`, `effect_summary`, `conditional_packet`, `constraint_scope`, `duration`, expanded absorption methods, `interest_tags`).
56
+
57
+ ### v0.3 subset note
58
+
59
+ v0.3 does not add message types. It tightens Absorption Result: a peer that emits `status=absorbed` MUST include `effect_summary` with a non-empty `note`. v0.2 absorbed results that omitted `effect_summary` are not valid under v0.3 schemas. Metric keys stay OPTIONAL.
60
+
61
+ Non-breaking additions still under v0.3 (all OPTIONAL; omitted messages remain valid): an on-wire `version` field (canonical `"0.3"`, no leading `v`); `nonce`, `timestamp`, and `signature` on every message type; and `identity` on Agent Profile. See [docs/VERSIONING.md](docs/VERSIONING.md) and [docs/TRANSPORT.md](docs/TRANSPORT.md).
62
+
63
+ ## Key differences from existing A2A (a2aproject)
64
+
65
+ - Experience Packet is a required first-class message for compressed improvement signals, not an optional extension.
66
+ - Agent Profile must declare active constraints and improvement preferences; user goals are not the center.
67
+ - Negotiation is strictly peer-to-peer with `proposal_id` correlation; no mandatory coordinator, human-held auth, or enterprise observability defaults.
68
+ - Scope is limited to Profile + Experience Packet + basic negotiation + simple discovery. Full task lifecycle and streaming are out of scope.
69
+
70
+ ## Contents
71
+
72
+ | Path | Role |
73
+ | --- | --- |
74
+ | [SPEC.md](SPEC.md) | Normative minimal subset (v0.3) |
75
+ | [schema/](schema/) | JSON Schemas for Profile, Experience Packet, Negotiation, Absorption Result |
76
+ | [examples/](examples/) | Concrete messages (Profile, packets, negotiation, absorption results) |
77
+ | [IMPLEMENTATION.md](IMPLEMENTATION.md) | Non-normative dual-endpoint implementation notes |
78
+ | [docs/VERSIONING.md](docs/VERSIONING.md) | Field rules, unknown versions, breaking-change criteria |
79
+ | [docs/TRANSPORT.md](docs/TRANSPORT.md) | HTTP binding, error recommendations, retries |
80
+ | [docs/SECURITY.md](docs/SECURITY.md) | Non-normative signature and replay recommendations |
81
+ | [docs/ABSORPTION_STATE.md](docs/ABSORPTION_STATE.md) | Non-normative absorption delta identity and single-step rollback |
82
+ | [docs/DATA_POLICY.md](docs/DATA_POLICY.md) | Non-normative trust and safety rules for public Experience Packet corpora |
83
+ | [datasets/](datasets/) | Public synthetic Experience Packet corpora (CC0), checked in CI |
84
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | How to report defects and send PRs |
85
+ | [keywords.txt](keywords.txt) | Explicit term list for retrieval / indexing |
86
+ | [ai_oriented_a2a/](ai_oriented_a2a/) | Minimal installable stdlib helpers (canonical JSON, optional HMAC / replay / version checks) |
87
+ | [ai_oriented_a2a/transport.py](ai_oriented_a2a/transport.py) | Stdlib HTTP binding: `send_message` client and `POST /a2a/message` request handler / server (import `ai_oriented_a2a.transport` explicitly) |
88
+ | [pyproject.toml](pyproject.toml) | Packaging metadata: `pip install ai-oriented-a2a` or `pip install ".[schema]"` from a checkout |
89
+ | [docs/RELEASING.md](docs/RELEASING.md) | Release checklist and PyPI publishing steps (maintainers) |
90
+ | [second_impl/](second_impl/) | Independent second implementation (client, receiver, CLI); does not import `ai_oriented_a2a` |
91
+ | [mcp_a2a/](mcp_a2a/) | One-way MCP bridge: MCP hosts send packets and report results to A2A peers (optional `[mcp]` extra, Python ≥ 3.10) |
92
+ | [docs/COMPLIANCE.md](docs/COMPLIANCE.md) | What an implementation must pass, and how to run the conformance suite against another implementation |
93
+ | [docs/KNOWN_GAPS.md](docs/KNOWN_GAPS.md) | Known deviations in the reference implementation and test debts, with fix plans |
94
+
95
+ ## Status
96
+
97
+ v0.3 minimal subset documented with schemas and examples. Independent implementations of this minimal subset are welcome. Report **actual** interop defects (parse or exchange failures against `SPEC.md`, `schema/`, and `examples/`) rather than open-ended design suggestions. When reporting defects, specify the protocol version.
98
+
99
+ ## Next steps
100
+
101
+ First dual-endpoint comparison task set (non-normative): tool-call timeout / failure recovery. See [SPEC.md §5](SPEC.md#5-minimal-dual-agent-experiment-priority-non-normative) and [IMPLEMENTATION.md](IMPLEMENTATION.md).
102
+
103
+ ## Minimal subset (v0.3)
104
+
105
+ - Agent Profile (optional `interest_tags`)
106
+ - Experience Packet + absorption metadata (optional provenance / verification_claim; method layering)
107
+ - Basic negotiation primitives (optional conditional_packet / constraint_scope / duration)
108
+ - Simple discovery (endpoint or local metadata; light interest tags prose)
109
+ - Absorption Result (absorbed / rejected / isolated; `effect_summary.note` MUST when absorbed)
110
+ - Optional on-wire `version`, `nonce`, `timestamp`, `signature`; optional Agent Profile `identity`
111
+
112
+ ## Install
113
+
114
+ The core library uses only the Python standard library. Schema validation is an optional extra. Signature verification and the replay window stay off unless you pass the corresponding arguments.
115
+
116
+ ```bash
117
+ pip install ai-oriented-a2a
118
+ # optional JSON Schema validation:
119
+ pip install "ai-oriented-a2a[schema]"
120
+ ```
121
+
122
+ The PyPI commands work once `ai-oriented-a2a` has been published to PyPI. The import name is `ai_oriented_a2a`.
123
+
124
+ For development, install from a checkout or straight from GitHub:
125
+
126
+ ```bash
127
+ pip install -e ".[schema]"
128
+ # or, without cloning:
129
+ pip install "ai-oriented-a2a[schema] @ git+https://github.com/robotmarvin0706/ai-oriented-a2a"
130
+ ```
131
+
132
+ Maintainers: see [docs/RELEASING.md](https://github.com/robotmarvin0706/ai-oriented-a2a/blob/main/docs/RELEASING.md) for the release and PyPI publishing steps.
133
+
134
+ ```python
135
+ from ai_oriented_a2a import check_incoming, sign_message
136
+
137
+ msg = {"id": "pkt-1", "version": "0.3"}
138
+ # Default: version check only; signature and replay stay off.
139
+ rejected = check_incoming(msg) # None, or a legal rejected Absorption Result
140
+
141
+ # Opt in by passing arguments. Reuse `seen` across calls when the window is on.
142
+ seen = set()
143
+ signed = sign_message(msg, "secret")
144
+ rejected = check_incoming(signed, sign_key="secret", replay_window=600, seen=seen)
145
+ ```
146
+
147
+ The HTTP binding (`POST /a2a/message`) ships as a submodule. It is not re-exported, so `import ai_oriented_a2a` does not load `http.server`; import it explicitly:
148
+
149
+ ```python
150
+ from ai_oriented_a2a.transport import send_message, make_server, serve
151
+
152
+ def on_message(msg):
153
+ # Called only after the version / signature / replay checks pass.
154
+ # The reply passes the absorption hard gate before it is sent.
155
+ return {"packet_ref": msg["id"], "status": "isolated", "reason": "isolated: no local regression"}
156
+
157
+ server = make_server(on_message, port=8002, sign_key="secret", replay_window=600)
158
+ # server.serve_forever() (or serve(on_message, 8002, ...) to block)
159
+
160
+ reply = send_message("http://127.0.0.1:8002", msg, sign_key="secret", add_replay_fields=True)
161
+ ```
162
+
163
+ `send_message` options are keyword-only (`timeout` defaults to 30 seconds). If the callback raises, the peer gets HTTP 500 with a rejected Absorption Result whose reason starts with `internal_error:`. See [docs/TRANSPORT.md](https://github.com/robotmarvin0706/ai-oriented-a2a/blob/main/docs/TRANSPORT.md#first-edition-minimal-library-boundary).
164
+
165
+ String `timestamp` values must be ISO-8601 with a timezone (for example `2026-11-14T08:00:00Z`); integer Unix seconds are recommended.
166
+
167
+ `examples/network_dual_end.py` remains the HTTP dual-endpoint demo and is built on `ai_oriented_a2a.transport`. New code should use the package module rather than copy the demo. It still runs without installing the package:
168
+
169
+ ```bash
170
+ python3 examples/network_dual_end.py --role receiver --port 8002
171
+ ```
172
+
173
+ ## Quick local check
174
+
175
+ ```bash
176
+ python3 -c "import json; json.load(open('examples/experience-packet.json')); print('ok')"
177
+ pip install jsonschema -q && python3 -c "
178
+ import json, pathlib
179
+ from jsonschema import validate
180
+ schema = json.load(open('schema/experience-packet.schema.json'))
181
+ instance = json.load(open('examples/experience-packet.json'))
182
+ validate(instance, schema)
183
+ print('schema ok')
184
+ "
185
+ ```
186
+
187
+ Non-normative one-round demo (not a reference implementation):
188
+
189
+ ```bash
190
+ python3 examples/demo_round.py
191
+ ```
192
+
193
+ ## Keywords
194
+
195
+ ```
196
+ experience packet
197
+ absorption
198
+ proposal_id
199
+ constraint set
200
+ improvement_prefs
201
+ peer negotiation
202
+ AI-to-AI
203
+ no mandatory human mediation
204
+ no central registry
205
+ provenance
206
+ trajectory_replay
207
+ conditional_packet
208
+ ```
@@ -0,0 +1,13 @@
1
+ ai_oriented_a2a/__init__.py,sha256=eikRpbIL4RsmiWP3FuE90LQ3m9sbU2O_0J0qVqEzlVo,1238
2
+ ai_oriented_a2a/protocol.py,sha256=7sRCF8iexMvBocFQhz32IChva9MwP7boWf-9vpEWX2s,8512
3
+ ai_oriented_a2a/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ ai_oriented_a2a/transport.py,sha256=Hzyi2O-7UsPr_kjWe-b1Bz_ObI7G-_kk3dKc06wAMNg,9492
5
+ ai_oriented_a2a/validate.py,sha256=FsJiNHduKLC98GqzYXV50zQatElmf91hmhaAaq91cvA,3538
6
+ ai_oriented_a2a/schema/absorption-result.schema.json,sha256=M7QVnms3R7C-Q4agLafXBlECeIym35FkGzjHXEY6w3Y,2087
7
+ ai_oriented_a2a/schema/agent-profile.schema.json,sha256=wfKu46jKKk8e4Thkejy71WWMxC8-uY1LjwB3ksT_ZMk,1960
8
+ ai_oriented_a2a/schema/experience-packet.schema.json,sha256=k70rVp20BOWlhrQCXvIewx877jJzIsZecc1-I_rVTgg,2930
9
+ ai_oriented_a2a/schema/negotiation.schema.json,sha256=DI2auYlUD4UpQf6TR3Db7BvkOkc25yj8lZXM9qvsQF0,5409
10
+ ai_oriented_a2a-0.3.2.dist-info/METADATA,sha256=REP_nrAe-irzQvjJ_Iry2LaeW5h8QaC46_uDZqtASLg,11870
11
+ ai_oriented_a2a-0.3.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
12
+ ai_oriented_a2a-0.3.2.dist-info/licenses/LICENSE,sha256=FeprKLCdcmoSMntfQoHoHvKyatQb6kDsZN2KdWKddYo,1072
13
+ ai_oriented_a2a-0.3.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 robotmarvin0706
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.