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.
- ai_oriented_a2a/__init__.py +51 -0
- ai_oriented_a2a/protocol.py +232 -0
- ai_oriented_a2a/py.typed +0 -0
- ai_oriented_a2a/schema/absorption-result.schema.json +59 -0
- ai_oriented_a2a/schema/agent-profile.schema.json +44 -0
- ai_oriented_a2a/schema/experience-packet.schema.json +80 -0
- ai_oriented_a2a/schema/negotiation.schema.json +118 -0
- ai_oriented_a2a/transport.py +273 -0
- ai_oriented_a2a/validate.py +93 -0
- ai_oriented_a2a-0.3.2.dist-info/METADATA +208 -0
- ai_oriented_a2a-0.3.2.dist-info/RECORD +13 -0
- ai_oriented_a2a-0.3.2.dist-info/WHEEL +4 -0
- ai_oriented_a2a-0.3.2.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
ai_oriented_a2a/py.typed
ADDED
|
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,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.
|