@7h3/protocol 0.1.2 → 0.5.0

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 (276) hide show
  1. package/.dockerignore +19 -0
  2. package/.github/dependabot.yml +32 -0
  3. package/.github/workflows/ci.yml +31 -0
  4. package/.github/workflows/publish.yml +59 -0
  5. package/.github/workflows/scorecard.yml +37 -0
  6. package/7h3.example.yaml +125 -0
  7. package/CHANGELOG.md +152 -0
  8. package/CONTRIBUTING.md +82 -0
  9. package/Dockerfile +73 -0
  10. package/GOVERNANCE.md +62 -0
  11. package/README.md +1317 -6
  12. package/SECURITY.md +70 -0
  13. package/bench-results/replay-cache-full-1777891033256.json +10 -0
  14. package/bench-results/replay-cache-full-1777896317488.json +10 -0
  15. package/bench-results/replay-cache-full-1777900993184.json +10 -0
  16. package/bench-results/replay-cache-full-1777901019285.json +10 -0
  17. package/bench-results/replay-cache-quick-1777870170126.json +10 -0
  18. package/bench-results/signature-profiles-quick-1775875160079.json +85 -0
  19. package/bench-results/signature-profiles-quick-1775983539716.json +85 -0
  20. package/bench-results/signature-profiles-quick-1776237913190.json +85 -0
  21. package/bench-results/wire-codecs-full-1777891019803.json +93 -0
  22. package/bench-results/wire-codecs-full-1777896260964.json +93 -0
  23. package/bench-results/wire-codecs-full-1777901004247.json +93 -0
  24. package/bench-results/wire-codecs-quick-1775972879056.json +93 -0
  25. package/bench-results/wire-codecs-quick-1775983541111.json +93 -0
  26. package/bench-results/wire-codecs-quick-1776237914299.json +93 -0
  27. package/bench-results/wire-codecs-quick-1777841285236.json +93 -0
  28. package/bench-results/wire-codecs-quick-1777841321772.json +93 -0
  29. package/bench-results/wire-codecs-quick-1777841330408.json +93 -0
  30. package/bench-results/wire-codecs-quick-1777852886082.json +93 -0
  31. package/bench-results/wire-codecs-quick-1777852988773.json +93 -0
  32. package/bench-results/wire-codecs-quick-1777870188095.json +93 -0
  33. package/bench-results/wire-codecs-quick-1777870263918.json +93 -0
  34. package/bench-results/wire-codecs-quick-1777870455034.json +93 -0
  35. package/bench-results/wire-codecs-quick-1778816163081.json +93 -0
  36. package/bench-results/wire-codecs-quick-1778843936130.json +93 -0
  37. package/bin/7h3.ts +406 -0
  38. package/conformance/7h3_v0_1.json +77 -0
  39. package/conformance/7h3_v0_1_binary.json +20 -0
  40. package/conformance/aip_v0_1_binary.json +20 -0
  41. package/docker-compose.yaml +77 -0
  42. package/docs/ADOPTION_PLAN.md +120 -0
  43. package/docs/AGENTS.md +77 -0
  44. package/docs/AIP_RFC_v0.1.md +97 -0
  45. package/docs/AI_DECISION_CARD.md +122 -0
  46. package/docs/AI_RUNTIME_POLICY.json +126 -0
  47. package/docs/AI_RUNTIME_POLICY.yaml +110 -0
  48. package/docs/BACKPRESSURE_TUNING.md +65 -0
  49. package/docs/BENCHMARK_CLAIM_MATRIX.md +42 -0
  50. package/docs/BENCHMARK_REPORT_TEMPLATE.md +169 -0
  51. package/docs/BINARY_CODEC_BENCH.md +23 -0
  52. package/docs/CLEAN_CLONE_RUNBOOK.md +36 -0
  53. package/docs/CLOCK_SKEW_POLICY.md +30 -0
  54. package/docs/DISTRIBUTED_REPLAY.md +142 -0
  55. package/docs/FUZZ_CAMPAIGN.md +121 -0
  56. package/docs/GATEWAY.md +195 -0
  57. package/docs/KEY_MANAGEMENT_POLICY.md +53 -0
  58. package/docs/KEY_REVOCATION.md +69 -0
  59. package/docs/MCP_WRAPPER.md +159 -0
  60. package/docs/MIGRATION_GUIDE.md +40 -0
  61. package/docs/OPERATORS.md +184 -0
  62. package/docs/PERF_REGRESSION_POLICY.md +34 -0
  63. package/docs/PROJECT_EXAMINATION_2026-05-31.md +219 -0
  64. package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +135 -0
  65. package/docs/RELEASE_GATE.md +25 -0
  66. package/docs/RELEASE_NOTES_v0.1.0.md +54 -0
  67. package/docs/SECURITY_REVIEW_2026-06-05.md +165 -0
  68. package/docs/TELEMETRY.md +41 -0
  69. package/docs/THREAT_MODEL.md +89 -0
  70. package/docs/VERSIONING_POLICY.md +30 -0
  71. package/docs/assets/banner-github.png +0 -0
  72. package/docs/assets/banner.png +0 -0
  73. package/docs/assets/banner.svg +123 -0
  74. package/eslint.config.js +15 -0
  75. package/fuzz/ts/harness-decode.ts +136 -0
  76. package/fuzz/ts/harness-verify.ts +121 -0
  77. package/fuzz/ts/run.ts +35 -0
  78. package/mcp-server/README.md +38 -0
  79. package/mcp-server/package-lock.json +1187 -0
  80. package/mcp-server/package.json +35 -0
  81. package/mcp-server/src/index.ts +236 -0
  82. package/mcp-server/tsconfig.json +14 -0
  83. package/package.json +121 -13
  84. package/scripts/aip-framework-quickstart.ts +110 -0
  85. package/scripts/aip-mcp-gateway.ts +38 -0
  86. package/scripts/aip-mcp-wrap-demo.ts +72 -0
  87. package/scripts/aip-quickstart.ts +60 -0
  88. package/scripts/bench-diff.ts +118 -0
  89. package/scripts/bench-protocol-e2e.ts +937 -0
  90. package/scripts/bench-protocol-openloop.ts +1397 -0
  91. package/scripts/bench-replay-cache.ts +76 -0
  92. package/scripts/bench-signature-profiles.ts +180 -0
  93. package/scripts/bench-wire-codecs.ts +161 -0
  94. package/scripts/build-binary-conformance.ts +36 -0
  95. package/scripts/build-release-dashboard.ts +175 -0
  96. package/scripts/canary-rollout.ts +38 -0
  97. package/scripts/mcpGatewayCli.test.ts +116 -0
  98. package/scripts/prepare-aip-package.ts +88 -0
  99. package/scripts/regen-conformance-sigs.ts +18 -0
  100. package/scripts/release-gate.ts +19 -0
  101. package/scripts/validate-runtime-policy.ts +18 -0
  102. package/sdk/browser/index.test.ts +162 -0
  103. package/sdk/browser/index.ts +257 -0
  104. package/sdk/browser/package.json +13 -0
  105. package/sdk/go/cbor.go +551 -0
  106. package/sdk/go/cbor_test.go +232 -0
  107. package/sdk/go/encryption.go +280 -0
  108. package/sdk/go/encryption_test.go +318 -0
  109. package/sdk/go/go.mod +7 -0
  110. package/sdk/go/go.sum +4 -0
  111. package/sdk/go/http.go +135 -0
  112. package/sdk/go/protocol.go +324 -0
  113. package/sdk/go/protocol_test.go +334 -0
  114. package/sdk/go/replay.go +121 -0
  115. package/sdk/go/replay_test.go +149 -0
  116. package/sdk/go/webhook.go +136 -0
  117. package/sdk/pq/package-lock.json +1358 -0
  118. package/sdk/pq/package.json +42 -0
  119. package/sdk/pq/src/index.test.ts +143 -0
  120. package/sdk/pq/src/index.ts +166 -0
  121. package/sdk/pq/tsconfig.json +14 -0
  122. package/sdk/pq/vitest.config.ts +7 -0
  123. package/sdk/python/README.md +18 -0
  124. package/sdk/python/protocol_7h3/__init__.py +46 -0
  125. package/sdk/python/protocol_7h3/encryption.py +252 -0
  126. package/sdk/python/protocol_7h3/http.py +212 -0
  127. package/sdk/python/protocol_7h3/keys.py +149 -0
  128. package/sdk/python/protocol_7h3/pq.py +244 -0
  129. package/sdk/python/protocol_7h3/protocol.py +525 -0
  130. package/sdk/python/protocol_7h3/queue.py +118 -0
  131. package/sdk/python/protocol_7h3/replay.py +98 -0
  132. package/sdk/python/protocol_7h3/webhook.py +116 -0
  133. package/sdk/python/pyproject.toml +40 -0
  134. package/sdk/python/tests/test_conformance.py +110 -0
  135. package/sdk/python/tests/test_encryption.py +206 -0
  136. package/sdk/python/tests/test_http.py +305 -0
  137. package/sdk/python/tests/test_keys.py +417 -0
  138. package/sdk/python/tests/test_queue.py +120 -0
  139. package/sdk/python/tests/test_webhook.py +345 -0
  140. package/sdk/rust/Cargo.lock +371 -0
  141. package/sdk/rust/Cargo.toml +25 -0
  142. package/sdk/rust/README.md +31 -0
  143. package/sdk/rust/fuzz/Cargo.toml +29 -0
  144. package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +46 -0
  145. package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +11 -0
  146. package/sdk/rust/src/bin/aip_mcp_gateway.rs +59 -0
  147. package/sdk/rust/src/http.rs +145 -0
  148. package/sdk/rust/src/keys.rs +161 -0
  149. package/sdk/rust/src/lib.rs +688 -0
  150. package/sdk/rust/src/queue.rs +79 -0
  151. package/sdk/rust/src/webhook.rs +86 -0
  152. package/sdk/rust/tests/conformance.rs +148 -0
  153. package/sdk/rust/tests/gateway.rs +130 -0
  154. package/sdk/rust/tests/http_webhook_queue.rs +201 -0
  155. package/sdk/rust/tests/keys.rs +189 -0
  156. package/sdk/threshold/index.d.ts +68 -0
  157. package/sdk/threshold/index.d.ts.map +1 -0
  158. package/sdk/threshold/index.js +254 -0
  159. package/sdk/threshold/package-lock.json +1361 -0
  160. package/sdk/threshold/package.json +39 -0
  161. package/sdk/threshold/src/index.d.ts +68 -0
  162. package/sdk/threshold/src/index.d.ts.map +1 -0
  163. package/sdk/threshold/src/index.js +254 -0
  164. package/sdk/threshold/src/index.test.ts +238 -0
  165. package/sdk/threshold/src/index.ts +355 -0
  166. package/sdk/threshold/tsconfig.json +19 -0
  167. package/sdk/threshold/vitest.config.ts +12 -0
  168. package/src/agentAdapter.test.ts +48 -0
  169. package/src/agentAdapter.ts +56 -0
  170. package/src/auditLog.test.ts +145 -0
  171. package/src/auditLog.ts +147 -0
  172. package/src/capability.test.ts +504 -0
  173. package/src/capability.ts +380 -0
  174. package/src/cborCodec.test.ts +263 -0
  175. package/src/cborCodec.ts +339 -0
  176. package/src/conformance.test.ts +136 -0
  177. package/src/conformanceVectors.ts +99 -0
  178. package/src/encryption.test.ts +206 -0
  179. package/src/encryption.ts +245 -0
  180. package/src/envelopeCbor.ts +140 -0
  181. package/src/frameworkAdapters.test.ts +290 -0
  182. package/src/frameworkAdapters.ts +261 -0
  183. package/src/gateway.test.ts +343 -0
  184. package/src/gateway.ts +246 -0
  185. package/src/grpcBinding.test.ts +211 -0
  186. package/src/grpcBinding.ts +103 -0
  187. package/src/httpBinding.test.ts +376 -0
  188. package/src/httpBinding.ts +189 -0
  189. package/src/index.ts +39 -0
  190. package/src/keyInfra.test.ts +278 -0
  191. package/src/keyInfra.ts +228 -0
  192. package/src/keyRegistry.ts +59 -0
  193. package/src/keyRotation.test.ts +78 -0
  194. package/src/keyRotation.ts +72 -0
  195. package/src/mcpGateway.test.ts +129 -0
  196. package/src/mcpGateway.ts +250 -0
  197. package/src/mcpTransports.test.ts +92 -0
  198. package/src/mcpTransports.ts +169 -0
  199. package/src/mcpWrapper.test.ts +179 -0
  200. package/src/mcpWrapper.ts +206 -0
  201. package/src/otel.ts +136 -0
  202. package/src/policyEnforcer.test.ts +99 -0
  203. package/src/policyEnforcer.ts +169 -0
  204. package/src/policyTelemetryFeedback.test.ts +25 -0
  205. package/src/policyTelemetryFeedback.ts +38 -0
  206. package/src/protocol.bench.ts +37 -0
  207. package/{protocol.d.ts → src/protocol.d.ts} +2 -1
  208. package/src/protocol.d.ts.map +1 -0
  209. package/src/protocol.js +294 -0
  210. package/src/protocol.test.ts +155 -0
  211. package/src/protocol.ts +414 -0
  212. package/src/protocolAgent.test.ts +105 -0
  213. package/src/protocolAgent.ts +169 -0
  214. package/src/protocolBinary.test.ts +165 -0
  215. package/src/protocolBinary.ts +312 -0
  216. package/src/protocolCapabilities.ts +70 -0
  217. package/src/protocolFuzz.advanced.test.ts +235 -0
  218. package/src/protocolFuzz.test.ts +111 -0
  219. package/src/protocolNegative.test.ts +97 -0
  220. package/src/protocolReplay.test.ts +71 -0
  221. package/src/protocolReplay.ts +194 -0
  222. package/src/protocolTransport.test.ts +556 -0
  223. package/src/protocolTransport.ts +483 -0
  224. package/src/queueBinding.test.ts +130 -0
  225. package/src/queueBinding.ts +102 -0
  226. package/src/rateLimiter.test.ts +96 -0
  227. package/src/rateLimiter.ts +46 -0
  228. package/src/redisClient.ts +140 -0
  229. package/src/redisIntegration.test.ts +134 -0
  230. package/src/replayStores.test.ts +273 -0
  231. package/src/replayStores.ts +215 -0
  232. package/src/revocation.test.ts +98 -0
  233. package/src/revocation.ts +0 -0
  234. package/src/routePolicy.test.ts +87 -0
  235. package/src/routePolicy.ts +72 -0
  236. package/src/runtimePolicy.test.ts +49 -0
  237. package/src/runtimePolicy.ts +81 -0
  238. package/src/runtimePolicyManager.test.ts +29 -0
  239. package/src/runtimePolicyManager.ts +50 -0
  240. package/src/runtimePolicyPresets.ts +43 -0
  241. package/src/signedResponse.test.ts +111 -0
  242. package/src/signedResponse.ts +83 -0
  243. package/src/stream.test.ts +254 -0
  244. package/src/stream.ts +417 -0
  245. package/src/telemetry.test.ts +251 -0
  246. package/src/telemetry.ts +299 -0
  247. package/src/webhookBinding.test.ts +144 -0
  248. package/src/webhookBinding.ts +115 -0
  249. package/src/wsBinding.test.ts +221 -0
  250. package/src/wsBinding.ts +200 -0
  251. package/tsconfig.json +15 -0
  252. package/tsconfig.lib.json +23 -0
  253. package/vite.lib.config.ts +16 -0
  254. package/vitest.config.ts +11 -0
  255. package/agentAdapter.d.ts +0 -26
  256. package/conformanceVectors.d.ts +0 -20
  257. package/frameworkAdapters.d.ts +0 -72
  258. package/index.d.ts +0 -20
  259. package/index.js +0 -1702
  260. package/keyRotation.d.ts +0 -20
  261. package/mcpGateway.d.ts +0 -37
  262. package/mcpTransports.d.ts +0 -62
  263. package/mcpWrapper.d.ts +0 -83
  264. package/policyEnforcer.d.ts +0 -50
  265. package/policyTelemetryFeedback.d.ts +0 -11
  266. package/protocolAgent.d.ts +0 -58
  267. package/protocolBinary.d.ts +0 -8
  268. package/protocolCapabilities.d.ts +0 -24
  269. package/protocolReplay.d.ts +0 -35
  270. package/protocolTransport.d.ts +0 -73
  271. package/redisClient.d.ts +0 -49
  272. package/replayStores.d.ts +0 -32
  273. package/revocation.d.ts +0 -71
  274. package/runtimePolicy.d.ts +0 -24
  275. package/runtimePolicyManager.d.ts +0 -15
  276. package/runtimePolicyPresets.d.ts +0 -11
@@ -0,0 +1,305 @@
1
+ """Tests for protocol_7h3.http — HTTP binding."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import time
6
+ import unittest
7
+
8
+ from protocol_7h3.protocol import sign_envelope_ed25519, sign_envelope_hmac
9
+ from protocol_7h3.http import (
10
+ DEFAULT_HEADER,
11
+ KeyRegistry,
12
+ StaticKeyRegistry,
13
+ build_signed_request_headers,
14
+ sign_http_request,
15
+ verify_http_envelope,
16
+ )
17
+
18
+ # ED25519 test key pair from conformance vectors (SPKI public / PKCS8 private, base64url)
19
+ _TEST_PUBLIC_KEY = "MCowBQYDK2VwAyEA-mUFiTQtcKN4nnD19V_-Wyy4q19OivnAutRUPhOcC78"
20
+ _TEST_PRIVATE_KEY = "MC4CAQAwBQYDK2VwBCIEICheZbQGuDVb6hezIlcs0QnCHGxz6IhiLkC9M0qr8OOZ"
21
+ _SENDER = "agent.ed"
22
+
23
+
24
+ def _make_valid_envelope(ttl_ms: int = 60_000) -> dict:
25
+ """Return an unsigned envelope with a current timestamp."""
26
+ now = int(time.time() * 1000)
27
+ return {
28
+ "header": {
29
+ "version": "7h3/0.1",
30
+ "messageId": "test-msg-1",
31
+ "timestampMs": now,
32
+ "ttlMs": ttl_ms,
33
+ "sender": _SENDER,
34
+ "nonce": "test-nonce-1",
35
+ },
36
+ "body": {
37
+ "intent": "TASK",
38
+ "content": "hello http",
39
+ },
40
+ }
41
+
42
+
43
+ def _signed_envelope(ttl_ms: int = 60_000) -> dict:
44
+ env = _make_valid_envelope(ttl_ms=ttl_ms)
45
+ return sign_envelope_ed25519(env, _TEST_PRIVATE_KEY)
46
+
47
+
48
+ def _registry() -> StaticKeyRegistry:
49
+ return StaticKeyRegistry({_SENDER: _TEST_PUBLIC_KEY})
50
+
51
+
52
+ class TestVerifyHttpEnvelopeMissingHeader(unittest.TestCase):
53
+ """1. verify_http_envelope: missing header."""
54
+
55
+ def test_missing_header_returns_false_and_reason(self):
56
+ ok, env, reason = verify_http_envelope({}, _registry())
57
+ self.assertFalse(ok)
58
+ self.assertIsNone(env)
59
+ self.assertEqual(reason, "missing-header")
60
+
61
+ def test_wrong_header_name_treated_as_missing(self):
62
+ headers = {"x-other-header": "{}"}
63
+ ok, env, reason = verify_http_envelope(headers, _registry())
64
+ self.assertFalse(ok)
65
+ self.assertEqual(reason, "missing-header")
66
+
67
+
68
+ class TestVerifyHttpEnvelopeInvalidJson(unittest.TestCase):
69
+ """2. verify_http_envelope: invalid JSON."""
70
+
71
+ def test_malformed_json(self):
72
+ headers = {DEFAULT_HEADER: "not-json{{{"}
73
+ ok, env, reason = verify_http_envelope(headers, _registry())
74
+ self.assertFalse(ok)
75
+ self.assertIsNone(env)
76
+ self.assertEqual(reason, "malformed-envelope")
77
+
78
+ def test_json_without_required_keys(self):
79
+ headers = {DEFAULT_HEADER: json.dumps({"foo": "bar"})}
80
+ ok, env, reason = verify_http_envelope(headers, _registry())
81
+ self.assertFalse(ok)
82
+ self.assertEqual(reason, "malformed-envelope")
83
+
84
+ def test_json_missing_signature_key(self):
85
+ headers = {DEFAULT_HEADER: json.dumps({"header": {}, "body": {}})}
86
+ ok, env, reason = verify_http_envelope(headers, _registry())
87
+ self.assertFalse(ok)
88
+ self.assertEqual(reason, "malformed-envelope")
89
+
90
+
91
+ class TestVerifyHttpEnvelopeValidEd25519(unittest.TestCase):
92
+ """3. verify_http_envelope: valid Ed25519 signed envelope."""
93
+
94
+ def test_valid_signed_envelope(self):
95
+ signed = _signed_envelope()
96
+ headers = {DEFAULT_HEADER: json.dumps(signed)}
97
+ ok, env, reason = verify_http_envelope(headers, _registry())
98
+ self.assertTrue(ok)
99
+ self.assertIsNotNone(env)
100
+ self.assertIsNone(reason)
101
+
102
+ def test_valid_envelope_mixed_case_header(self):
103
+ """Headers are case-insensitive per HTTP spec."""
104
+ signed = _signed_envelope()
105
+ headers = {"X-7H3-Envelope": json.dumps(signed)}
106
+ ok, env, reason = verify_http_envelope(headers, _registry())
107
+ self.assertTrue(ok)
108
+ self.assertIsNone(reason)
109
+
110
+ def test_returns_envelope_dict_on_success(self):
111
+ signed = _signed_envelope()
112
+ headers = {DEFAULT_HEADER: json.dumps(signed)}
113
+ ok, env, reason = verify_http_envelope(headers, _registry())
114
+ self.assertTrue(ok)
115
+ self.assertIn("header", env)
116
+ self.assertIn("body", env)
117
+ self.assertIn("signature", env)
118
+
119
+
120
+ class TestVerifyHttpEnvelopeTampered(unittest.TestCase):
121
+ """4. verify_http_envelope: tampered envelope."""
122
+
123
+ def test_tampered_content_fails_verification(self):
124
+ signed = _signed_envelope()
125
+ # Tamper with the body content after signing
126
+ signed["body"]["content"] = "tampered!"
127
+ headers = {DEFAULT_HEADER: json.dumps(signed)}
128
+ ok, env, reason = verify_http_envelope(headers, _registry())
129
+ self.assertFalse(ok)
130
+ self.assertEqual(reason, "invalid-signature")
131
+
132
+ def test_tampered_sender_fails_verification(self):
133
+ signed = _signed_envelope()
134
+ signed["header"]["sender"] = "evil.agent"
135
+ headers = {DEFAULT_HEADER: json.dumps(signed)}
136
+ # evil.agent not in registry → unknown-sender
137
+ ok, env, reason = verify_http_envelope(headers, _registry())
138
+ self.assertFalse(ok)
139
+ self.assertIn(reason, ("unknown-sender", "invalid-signature"))
140
+
141
+ def test_tampered_signature_value_fails(self):
142
+ signed = _signed_envelope()
143
+ signed["signature"]["value"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
144
+ headers = {DEFAULT_HEADER: json.dumps(signed)}
145
+ ok, env, reason = verify_http_envelope(headers, _registry())
146
+ self.assertFalse(ok)
147
+ self.assertEqual(reason, "invalid-signature")
148
+
149
+
150
+ class TestVerifyHttpEnvelopeUnknownSender(unittest.TestCase):
151
+ """5. verify_http_envelope: unknown sender."""
152
+
153
+ def test_unknown_sender_ed25519(self):
154
+ signed = _signed_envelope()
155
+ # Empty registry — no known senders
156
+ empty_registry = StaticKeyRegistry({})
157
+ headers = {DEFAULT_HEADER: json.dumps(signed)}
158
+ ok, env, reason = verify_http_envelope(headers, empty_registry)
159
+ self.assertFalse(ok)
160
+ self.assertIsNone(env)
161
+ self.assertEqual(reason, "unknown-sender")
162
+
163
+ def test_unknown_sender_hmac(self):
164
+ env = _make_valid_envelope()
165
+ signed = sign_envelope_hmac(env, "mysecret", "key-1")
166
+ headers = {DEFAULT_HEADER: json.dumps(signed)}
167
+
168
+ class NoSecretRegistry(KeyRegistry):
169
+ def get_public_key(self, sender_id: str):
170
+ return None
171
+ def get_shared_secret(self, key_id: str):
172
+ return None # key_id "key-1" unknown
173
+
174
+ ok, result, reason = verify_http_envelope(headers, NoSecretRegistry())
175
+ self.assertFalse(ok)
176
+ self.assertEqual(reason, "unknown-sender")
177
+
178
+
179
+ class TestSignHttpRequestRoundTrip(unittest.TestCase):
180
+ """6. sign_http_request + verify_http_envelope round trip."""
181
+
182
+ def test_round_trip_succeeds(self):
183
+ unsigned = _make_valid_envelope()
184
+ headers = sign_http_request(unsigned, _TEST_PRIVATE_KEY)
185
+ ok, env, reason = verify_http_envelope(headers, _registry())
186
+ self.assertTrue(ok)
187
+ self.assertIsNone(reason)
188
+ self.assertEqual(env["body"]["content"], "hello http")
189
+
190
+ def test_round_trip_strict_ttl_expired(self):
191
+ # Create envelope that is already expired
192
+ past_ms = int(time.time() * 1000) - 120_000 # 2 minutes ago
193
+ envelope = {
194
+ "header": {
195
+ "version": "7h3/0.1",
196
+ "messageId": "expired-msg",
197
+ "timestampMs": past_ms,
198
+ "ttlMs": 1_000, # 1 second TTL → already expired
199
+ "sender": _SENDER,
200
+ "nonce": "nonce-x",
201
+ },
202
+ "body": {
203
+ "intent": "TASK",
204
+ "content": "stale",
205
+ },
206
+ }
207
+ signed = sign_envelope_ed25519(envelope, _TEST_PRIVATE_KEY)
208
+ headers = {DEFAULT_HEADER: json.dumps(signed)}
209
+ ok, env, reason = verify_http_envelope(headers, _registry(), strict_ttl=True)
210
+ self.assertFalse(ok)
211
+ self.assertEqual(reason, "ttl-expired")
212
+
213
+ def test_round_trip_strict_ttl_disabled(self):
214
+ # Same expired envelope but strict_ttl=False → signature still valid
215
+ past_ms = int(time.time() * 1000) - 120_000
216
+ envelope = {
217
+ "header": {
218
+ "version": "7h3/0.1",
219
+ "messageId": "expired-msg-2",
220
+ "timestampMs": past_ms,
221
+ "ttlMs": 1_000,
222
+ "sender": _SENDER,
223
+ "nonce": "nonce-y",
224
+ },
225
+ "body": {
226
+ "intent": "TASK",
227
+ "content": "stale but skip ttl check",
228
+ },
229
+ }
230
+ signed = sign_envelope_ed25519(envelope, _TEST_PRIVATE_KEY)
231
+ headers = {DEFAULT_HEADER: json.dumps(signed)}
232
+ ok, env, reason = verify_http_envelope(headers, _registry(), strict_ttl=False)
233
+ self.assertTrue(ok)
234
+ self.assertIsNone(reason)
235
+
236
+ def test_custom_header_name(self):
237
+ unsigned = _make_valid_envelope()
238
+ custom_hdr = "x-my-agent-envelope"
239
+ headers = sign_http_request(unsigned, _TEST_PRIVATE_KEY, header_name=custom_hdr)
240
+ self.assertIn(custom_hdr, headers)
241
+ ok, env, reason = verify_http_envelope(headers, _registry(), header_name=custom_hdr)
242
+ self.assertTrue(ok)
243
+ self.assertIsNone(reason)
244
+
245
+
246
+ class TestBuildSignedRequestHeaders(unittest.TestCase):
247
+ """7. build_signed_request_headers produces correct header key."""
248
+
249
+ def test_default_header_key_present(self):
250
+ headers = build_signed_request_headers(
251
+ sender=_SENDER,
252
+ private_key=_TEST_PRIVATE_KEY,
253
+ content="ping",
254
+ )
255
+ self.assertIn(DEFAULT_HEADER, headers)
256
+
257
+ def test_custom_header_key(self):
258
+ custom = "x-aip-envelope"
259
+ headers = build_signed_request_headers(
260
+ sender=_SENDER,
261
+ private_key=_TEST_PRIVATE_KEY,
262
+ content="ping",
263
+ header_name=custom,
264
+ )
265
+ self.assertIn(custom, headers)
266
+ self.assertNotIn(DEFAULT_HEADER, headers)
267
+
268
+ def test_header_value_is_valid_json_envelope(self):
269
+ headers = build_signed_request_headers(
270
+ sender=_SENDER,
271
+ private_key=_TEST_PRIVATE_KEY,
272
+ content="check me",
273
+ ttl_ms=30_000,
274
+ )
275
+ raw = headers[DEFAULT_HEADER]
276
+ envelope = json.loads(raw)
277
+ self.assertEqual(envelope["header"]["sender"], _SENDER)
278
+ self.assertEqual(envelope["body"]["content"], "check me")
279
+ self.assertEqual(envelope["signature"]["alg"], "ED25519")
280
+
281
+ def test_round_trip_verify(self):
282
+ headers = build_signed_request_headers(
283
+ sender=_SENDER,
284
+ private_key=_TEST_PRIVATE_KEY,
285
+ content="full round trip",
286
+ recipient="agent.receiver",
287
+ )
288
+ ok, env, reason = verify_http_envelope(headers, _registry())
289
+ self.assertTrue(ok)
290
+ self.assertIsNone(reason)
291
+ self.assertEqual(env["body"]["content"], "full round trip")
292
+
293
+ def test_recipient_is_included_when_provided(self):
294
+ headers = build_signed_request_headers(
295
+ sender=_SENDER,
296
+ private_key=_TEST_PRIVATE_KEY,
297
+ content="msg",
298
+ recipient="agent.b",
299
+ )
300
+ envelope = json.loads(headers[DEFAULT_HEADER])
301
+ self.assertEqual(envelope["header"].get("recipient"), "agent.b")
302
+
303
+
304
+ if __name__ == "__main__":
305
+ unittest.main()