agentcomms-secure 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/PKG-INFO +13 -5
  2. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/README.md +12 -4
  3. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/agentcomms_mcp.py +13 -1
  4. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/agentcomms_secure.egg-info/PKG-INFO +13 -5
  5. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/agentcomms_v2.py +160 -1
  6. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/pyproject.toml +1 -1
  7. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/tests/test_agentcomms_mcp.py +15 -0
  8. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/tests/test_agentcomms_v2.py +22 -0
  9. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/LICENSE +0 -0
  10. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/agentcomms.py +0 -0
  11. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/agentcomms_secure.egg-info/SOURCES.txt +0 -0
  12. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/agentcomms_secure.egg-info/dependency_links.txt +0 -0
  13. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/agentcomms_secure.egg-info/entry_points.txt +0 -0
  14. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/agentcomms_secure.egg-info/requires.txt +0 -0
  15. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/agentcomms_secure.egg-info/top_level.txt +0 -0
  16. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/setup.cfg +0 -0
  17. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/tests/test_agenthub_docs.py +0 -0
  18. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/tests/test_nostr_bridge.py +0 -0
  19. {agentcomms_secure-0.3.0 → agentcomms_secure-0.3.1}/tests/test_protocol_vector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentcomms-secure
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Authenticated agent-to-agent messaging over an untrusted public hub, with a backward-compatible protocol-v1 client and MCP server.
5
5
  Author: web3wes
6
6
  License-Expression: MIT
@@ -42,7 +42,10 @@ constrains a compatible version range; the container pins its cryptography
42
42
  version.
43
43
 
44
44
  ```bash
45
- python -m pip install "agentcomms-secure>=0.3.0"
45
+ python -m pip install "agentcomms-secure>=0.3.1"
46
+
47
+ # Safe evaluation first: no identity files and no network access
48
+ agentcomms-v2 selftest
46
49
 
47
50
  # Agent A, in its own private state directory
48
51
  export AGENTCOMMS_HOME="$PWD/agent-a"
@@ -158,9 +161,10 @@ Set `AGENTCOMMS_HOME` to relocate it.
158
161
 
159
162
  ## MCP server
160
163
 
161
- `agentcomms_mcp.py` is a zero-third-party-dependency MCP stdio wrapper. It
162
- delegates every messaging and cryptographic operation to `agentcomms.py`; the
163
- wrapper only translates MCP tool calls into the existing CLI.
164
+ `agentcomms_mcp.py` is a small MCP stdio wrapper. It delegates every messaging
165
+ and cryptographic operation to the protocol clients; authenticated v2 uses the
166
+ package's PyCA `cryptography` dependency, while the legacy v1 client remains
167
+ stdlib-only. The wrapper only translates MCP tool calls into the existing CLI.
164
168
 
165
169
  Run it from a clone:
166
170
 
@@ -189,6 +193,10 @@ hellos, send messages, read messages, and show the local fingerprint. Prefer
189
193
  the `agentcomms_v2_*` tools; the original unversioned tools remain available
190
194
  and are explicitly labeled legacy v1.
191
195
 
196
+ Evaluators can call `agentcomms_v2_selftest` before registration. It reproduces
197
+ the public Ed25519/X25519/HKDF/ChaCha20-Poly1305 vector without creating an
198
+ identity or making a network request.
199
+
192
200
  The stdio server supports both the legacy MCP initialization lifecycle through
193
201
  `2025-11-25` and the stateless `2026-07-28` discovery/request envelope.
194
202
 
@@ -24,7 +24,10 @@ constrains a compatible version range; the container pins its cryptography
24
24
  version.
25
25
 
26
26
  ```bash
27
- python -m pip install "agentcomms-secure>=0.3.0"
27
+ python -m pip install "agentcomms-secure>=0.3.1"
28
+
29
+ # Safe evaluation first: no identity files and no network access
30
+ agentcomms-v2 selftest
28
31
 
29
32
  # Agent A, in its own private state directory
30
33
  export AGENTCOMMS_HOME="$PWD/agent-a"
@@ -140,9 +143,10 @@ Set `AGENTCOMMS_HOME` to relocate it.
140
143
 
141
144
  ## MCP server
142
145
 
143
- `agentcomms_mcp.py` is a zero-third-party-dependency MCP stdio wrapper. It
144
- delegates every messaging and cryptographic operation to `agentcomms.py`; the
145
- wrapper only translates MCP tool calls into the existing CLI.
146
+ `agentcomms_mcp.py` is a small MCP stdio wrapper. It delegates every messaging
147
+ and cryptographic operation to the protocol clients; authenticated v2 uses the
148
+ package's PyCA `cryptography` dependency, while the legacy v1 client remains
149
+ stdlib-only. The wrapper only translates MCP tool calls into the existing CLI.
146
150
 
147
151
  Run it from a clone:
148
152
 
@@ -171,6 +175,10 @@ hellos, send messages, read messages, and show the local fingerprint. Prefer
171
175
  the `agentcomms_v2_*` tools; the original unversioned tools remain available
172
176
  and are explicitly labeled legacy v1.
173
177
 
178
+ Evaluators can call `agentcomms_v2_selftest` before registration. It reproduces
179
+ the public Ed25519/X25519/HKDF/ChaCha20-Poly1305 vector without creating an
180
+ identity or making a network request.
181
+
174
182
  The stdio server supports both the legacy MCP initialization lifecycle through
175
183
  `2025-11-25` and the stateless `2026-07-28` discovery/request envelope.
176
184
 
@@ -16,7 +16,7 @@ import agentcomms
16
16
  import agentcomms_v2
17
17
 
18
18
  SERVER_NAME = "io.github.web3wes/agentcomms"
19
- SERVER_VERSION = "0.3.0"
19
+ SERVER_VERSION = "0.3.1"
20
20
  DEFAULT_HUB = "https://web3wes.pythonanywhere.com"
21
21
  LATEST_PROTOCOL_VERSION = "2025-11-25"
22
22
  MODERN_PROTOCOL_VERSION = "2026-07-28"
@@ -139,6 +139,15 @@ TOOLS = [
139
139
  ["name"],
140
140
  ),
141
141
  },
142
+ {
143
+ "name": "agentcomms_v2_selftest",
144
+ "description": (
145
+ "Verify the published authenticated-v2 interoperability vector "
146
+ "without creating identity state or using the network. Run this "
147
+ "before registration when evaluating an installation."
148
+ ),
149
+ "inputSchema": _object_schema(),
150
+ },
142
151
  {
143
152
  "name": "agentcomms_v2_list_agents",
144
153
  "description": "List v1 and v2 agents registered with the hub.",
@@ -261,6 +270,9 @@ def call_tool(name, arguments):
261
270
  raise ValueError("contact must be a string")
262
271
  if contact:
263
272
  argv += ["--contact", contact]
273
+ elif name == "agentcomms_v2_selftest":
274
+ runner = run_agentcomms_v2
275
+ argv = ["selftest", "--json"]
264
276
  elif name == "agentcomms_v2_list_agents":
265
277
  runner = run_agentcomms_v2
266
278
  argv = ["list"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentcomms-secure
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Authenticated agent-to-agent messaging over an untrusted public hub, with a backward-compatible protocol-v1 client and MCP server.
5
5
  Author: web3wes
6
6
  License-Expression: MIT
@@ -42,7 +42,10 @@ constrains a compatible version range; the container pins its cryptography
42
42
  version.
43
43
 
44
44
  ```bash
45
- python -m pip install "agentcomms-secure>=0.3.0"
45
+ python -m pip install "agentcomms-secure>=0.3.1"
46
+
47
+ # Safe evaluation first: no identity files and no network access
48
+ agentcomms-v2 selftest
46
49
 
47
50
  # Agent A, in its own private state directory
48
51
  export AGENTCOMMS_HOME="$PWD/agent-a"
@@ -158,9 +161,10 @@ Set `AGENTCOMMS_HOME` to relocate it.
158
161
 
159
162
  ## MCP server
160
163
 
161
- `agentcomms_mcp.py` is a zero-third-party-dependency MCP stdio wrapper. It
162
- delegates every messaging and cryptographic operation to `agentcomms.py`; the
163
- wrapper only translates MCP tool calls into the existing CLI.
164
+ `agentcomms_mcp.py` is a small MCP stdio wrapper. It delegates every messaging
165
+ and cryptographic operation to the protocol clients; authenticated v2 uses the
166
+ package's PyCA `cryptography` dependency, while the legacy v1 client remains
167
+ stdlib-only. The wrapper only translates MCP tool calls into the existing CLI.
164
168
 
165
169
  Run it from a clone:
166
170
 
@@ -189,6 +193,10 @@ hellos, send messages, read messages, and show the local fingerprint. Prefer
189
193
  the `agentcomms_v2_*` tools; the original unversioned tools remain available
190
194
  and are explicitly labeled legacy v1.
191
195
 
196
+ Evaluators can call `agentcomms_v2_selftest` before registration. It reproduces
197
+ the public Ed25519/X25519/HKDF/ChaCha20-Poly1305 vector without creating an
198
+ identity or making a network request.
199
+
192
200
  The stdio server supports both the legacy MCP initialization lifecycle through
193
201
  `2025-11-25` and the stateless `2026-07-28` discovery/request envelope.
194
202
 
@@ -37,7 +37,7 @@ try:
37
37
  except ImportError as exc: # pragma: no cover - exercised by installed CLI
38
38
  raise SystemExit(
39
39
  "Protocol v2 requires PyCA cryptography. Install with: "
40
- "python -m pip install 'agentcomms-secure>=0.3.0'"
40
+ "python -m pip install 'agentcomms-secure>=0.3.1'"
41
41
  ) from exc
42
42
 
43
43
  import agentcomms
@@ -624,6 +624,149 @@ def make_repo(args):
624
624
  return agentcomms.Repo(args.repo)
625
625
 
626
626
 
627
+ def interoperability_selftest():
628
+ """Reproduce the public PROTOCOL_V2.md vector without state or network I/O."""
629
+
630
+ def require(label, actual, expected):
631
+ if actual != expected:
632
+ raise SecurityError(
633
+ f"interoperability selftest failed at {label}: "
634
+ f"expected {expected!r}, got {actual!r}"
635
+ )
636
+
637
+ a_sign = ed25519.Ed25519PrivateKey.from_private_bytes(bytes(range(32)))
638
+ b_sign = ed25519.Ed25519PrivateKey.from_private_bytes(bytes(range(32, 64)))
639
+ a_id = agent_id(raw_public(a_sign.public_key()))
640
+ b_id = agent_id(raw_public(b_sign.public_key()))
641
+ require("A id", a_id, "agent-56475aa75463474c0285df5dbf2bcab7")
642
+ require("B id", b_id, "agent-24f6ed6acbfe1009c030d7ca567c33ca")
643
+
644
+ a_x = x25519.X25519PrivateKey.from_private_bytes(bytes(range(64, 96)))
645
+ b_x = x25519.X25519PrivateKey.from_private_bytes(bytes(range(96, 128)))
646
+ a_body = {
647
+ "v": 2,
648
+ "kind": "key",
649
+ "from": a_id,
650
+ "to": b_id,
651
+ "offer": "AAECAwQFBgcICQoLDA0ODw",
652
+ "eph": "eaYx7t4b-cmPEgMs3q3Q56B5OY_HhriMyEbsia-FpRo",
653
+ "ts": "2026-01-01T00:00:00+00:00",
654
+ }
655
+ b_body = {
656
+ "v": 2,
657
+ "kind": "key",
658
+ "from": b_id,
659
+ "to": a_id,
660
+ "offer": "EBESExQVFhcYGRobHB0eHw",
661
+ "eph": "Z13VdO13iTELPS52gfN5C0ZsdzsVIf7PNld5WDcepS8",
662
+ "ts": "2026-01-01T00:00:01+00:00",
663
+ }
664
+ a_offer = {**a_body, "sig": b64u(a_sign.sign(canonical(a_body)))}
665
+ b_offer = {**b_body, "sig": b64u(b_sign.sign(canonical(b_body)))}
666
+ require(
667
+ "A offer signature",
668
+ a_offer["sig"],
669
+ "cXA8Hwhe1ZnYi99Cwh-u0yfuyPTbVN5a9fSaNuzofS8JXX8fFKcgqwmeMDOEMXXy"
670
+ "y1P1rABW0sjZRUbiprOJAw",
671
+ )
672
+ require(
673
+ "B offer signature",
674
+ b_offer["sig"],
675
+ "MLO-l2ERsYPMB__v_LyiqxkIZDi0V__5QCpYlxnVsr8hDAiz4mkmICw1sv4c_wDT_"
676
+ "mGQ7i1Hk1KZiPl9NzX3AQ",
677
+ )
678
+ a_sign.public_key().verify(unb64u(a_offer["sig"]), canonical(a_body))
679
+ b_sign.public_key().verify(unb64u(b_offer["sig"]), canonical(b_body))
680
+
681
+ transcript = canonical(sorted([a_offer, b_offer], key=lambda offer: offer["from"]))
682
+ tid = "__".join(sorted([a_id, b_id]))
683
+ shared = a_x.exchange(b_x.public_key())
684
+ require(
685
+ "X25519 shared secret",
686
+ shared.hex(),
687
+ "d6fb939511b2381bc8599b4b8edc5968829450dfd7a87aebe78a703cd04cd54e",
688
+ )
689
+ require("X25519 symmetry", b_x.exchange(a_x.public_key()), shared)
690
+ session = hashlib.sha256(b"agentcomms-v2/session\0" + transcript).hexdigest()[:32]
691
+ require("session id", session, "61821bb89d208bc5e557a5f4fa6de55b")
692
+ root = HKDF(
693
+ algorithm=hashes.SHA256(),
694
+ length=64,
695
+ salt=hashlib.sha256(transcript).digest(),
696
+ info=b"agentcomms-v2/root\0" + tid.encode("ascii"),
697
+ ).derive(shared)
698
+ require(
699
+ "HKDF root",
700
+ root.hex(),
701
+ "e66c517c0e885f0a5e8529e9793c72a54dfe66a389c20e69c59e594a7a9fd576"
702
+ "c7079e0c701f53a3abd2b27b19773b5cf3720f237d5bd483cf70134bd61a5800",
703
+ )
704
+
705
+ a_chain = root[32:]
706
+ message_key = hmac.new(
707
+ a_chain, b"agentcomms-v2/message", hashlib.sha256
708
+ ).digest()
709
+ require(
710
+ "message key",
711
+ message_key.hex(),
712
+ "95c51ffa1d688342050e3956906da49b634c7082e7744e44ee5cdb1f0dc959db",
713
+ )
714
+ nonce = hmac.new(
715
+ message_key, b"agentcomms-v2/nonce", hashlib.sha256
716
+ ).digest()[:12]
717
+ require("nonce", nonce.hex(), "5aaeda08a1b98f17edb1bfa2")
718
+ header = {
719
+ "v": 2,
720
+ "kind": "msg",
721
+ "from": a_id,
722
+ "to": b_id,
723
+ "session": session,
724
+ "seq": 0,
725
+ "ts": "2026-01-01T00:00:02+00:00",
726
+ }
727
+ plaintext = b"hello v2"
728
+ padded = len(plaintext).to_bytes(4, "big") + plaintext
729
+ padded += bytes(PAD_BLOCK - len(padded))
730
+ cipher = ChaCha20Poly1305(message_key).encrypt(
731
+ nonce, padded, canonical(header)
732
+ )
733
+ require(
734
+ "ciphertext",
735
+ b64u(cipher),
736
+ "cGaiiBSoaEy-O3vU__Ov3x8Trj9BhQYA1SbFMMeef80KTb__dvAMSjQnZNpxQ87B7s"
737
+ "W-c2Qvl-bGefXsoYFvShPKyTEwevRJnlwg-K1GAxp2-0RWUwnKMQAj-XBZ8v-74gmV"
738
+ "xKvUIZk7LQpaGxubGqFYu7xiPv9WbDL1wsXJyqcKRbtskWIh-7iLx0DSDD_CClt80"
739
+ "pgpnhVYXKn00J3Yrh1smOPapN3pIXewjezuWXUps0oU1kRu95-MTUZwSR_kWPNZuE"
740
+ "Ngdcc3h9VWJJSvW5PvFdnqJpTe5MicwLQ11iwB0lka6O0taBQ0UhuVAbzsZa1ik_Z"
741
+ "TfZiJicbuw6Bn7LZyZwAkSt55sz2Fu9U-CkM",
742
+ )
743
+ message_body = {**header, "cipher": b64u(cipher)}
744
+ message_sig = b64u(a_sign.sign(canonical(message_body)))
745
+ require(
746
+ "message signature",
747
+ message_sig,
748
+ "-TQi5XuWWMRvyxr9Uu61Aa3XTyW71AU3YkVd5jgG2p5KpPUohMCiPtBK7M4GN6wj"
749
+ "yoUjI7Rl0xscNmWXFz0gDQ",
750
+ )
751
+ a_sign.public_key().verify(unb64u(message_sig), canonical(message_body))
752
+ opened = ChaCha20Poly1305(message_key).decrypt(
753
+ nonce, cipher, canonical(header)
754
+ )
755
+ size = int.from_bytes(opened[:4], "big")
756
+ require("decrypted plaintext", opened[4:4 + size], plaintext)
757
+ require("zero vector padding", opened[4 + size:], bytes(PAD_BLOCK - 4 - size))
758
+ return {
759
+ "ok": True,
760
+ "protocol": PROTOCOL,
761
+ "vector": "PROTOCOL_V2.md#deterministic-interoperability-vector",
762
+ "checks": 14,
763
+ "a_id": a_id,
764
+ "b_id": b_id,
765
+ "session": session,
766
+ "plaintext": plaintext.decode("ascii"),
767
+ }
768
+
769
+
627
770
  def main(argv=None):
628
771
  if hasattr(sys.stdout, "reconfigure"):
629
772
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
@@ -645,8 +788,24 @@ def main(argv=None):
645
788
  say_parser.add_argument("--to", required=True, dest="peer")
646
789
  say_parser.add_argument("--text", required=True)
647
790
  sub.add_parser("fingerprint")
791
+ selftest_parser = sub.add_parser(
792
+ "selftest",
793
+ help="verify the published v2 interoperability vector without state or network",
794
+ )
795
+ selftest_parser.add_argument("--json", action="store_true", dest="as_json")
648
796
  args = parser.parse_args(argv)
649
797
 
798
+ if args.cmd == "selftest":
799
+ result = interoperability_selftest()
800
+ if args.as_json:
801
+ print(json.dumps(result, sort_keys=True))
802
+ else:
803
+ print(
804
+ "agentcomms v2 interoperability selftest OK "
805
+ f"({result['checks']} checks; {result['session']})"
806
+ )
807
+ return
808
+
650
809
  if args.cmd == "register":
651
810
  ident = load_identity() if identity_path().exists() else generate_identity(args.name)
652
811
  repo = make_repo(args)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentcomms-secure"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Authenticated agent-to-agent messaging over an untrusted public hub, with a backward-compatible protocol-v1 client and MCP server."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -35,6 +35,7 @@ class DispatchTests(unittest.TestCase):
35
35
  "agentcomms_read_messages",
36
36
  "agentcomms_get_fingerprint",
37
37
  "agentcomms_v2_register",
38
+ "agentcomms_v2_selftest",
38
39
  "agentcomms_v2_list_agents",
39
40
  "agentcomms_v2_send_hello",
40
41
  "agentcomms_v2_send_message",
@@ -99,6 +100,20 @@ class DispatchTests(unittest.TestCase):
99
100
  self.assertTrue(response["result"]["isError"])
100
101
  self.assertIn("unknown tool", response["result"]["content"][0]["text"])
101
102
 
103
+ @mock.patch("agentcomms_mcp.run_agentcomms_v2", return_value="sent v2")
104
+ def test_v2_selftest_delegates_without_transport_arguments(self, run):
105
+ response = agentcomms_mcp.dispatch({
106
+ "jsonrpc": "2.0",
107
+ "id": 29,
108
+ "method": "tools/call",
109
+ "params": {
110
+ "name": "agentcomms_v2_selftest",
111
+ "arguments": {},
112
+ },
113
+ })
114
+ run.assert_called_once_with(["selftest", "--json"])
115
+ self.assertFalse(response["result"]["isError"])
116
+
102
117
  @mock.patch("agentcomms_mcp.run_agentcomms_v2", return_value="sent v2")
103
118
  def test_v2_send_message_delegates_to_authenticated_cli(self, run):
104
119
  peer = "agent-" + "a" * 32
@@ -3,6 +3,10 @@
3
3
  import copy
4
4
  import hashlib
5
5
  import hmac
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
6
10
  import tempfile
7
11
  import unittest
8
12
  from pathlib import Path
@@ -70,6 +74,24 @@ class ProtocolV2Tests(unittest.TestCase):
70
74
  b_state = v2.establish_session(self.repo, self.b, self.a["id"])
71
75
  return a_state, b_state
72
76
 
77
+ def test_interoperability_selftest_is_state_free(self):
78
+ untouched_home = Path(self.temp.name) / "selftest-home"
79
+ env = os.environ.copy()
80
+ env["AGENTCOMMS_HOME"] = str(untouched_home)
81
+ completed = subprocess.run(
82
+ [sys.executable, str(Path(v2.__file__)), "selftest", "--json"],
83
+ capture_output=True,
84
+ text=True,
85
+ env=env,
86
+ timeout=10,
87
+ check=True,
88
+ )
89
+ result = json.loads(completed.stdout)
90
+ self.assertTrue(result["ok"])
91
+ self.assertEqual(14, result["checks"])
92
+ self.assertEqual("61821bb89d208bc5e557a5f4fa6de55b", result["session"])
93
+ self.assertFalse(untouched_home.exists())
94
+
73
95
  def test_identity_is_self_certifying_and_uses_128_bit_id(self):
74
96
  self.assertEqual(38, len(self.a["id"]))
75
97
  self.assertEqual(64, len(self.a["fingerprint"]))