@7h3/protocol 0.1.2 → 0.4.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 (230) 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 +92 -0
  8. package/CONTRIBUTING.md +82 -0
  9. package/Dockerfile +73 -0
  10. package/GOVERNANCE.md +62 -0
  11. package/README.md +323 -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 +385 -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.png +0 -0
  72. package/eslint.config.js +15 -0
  73. package/fuzz/ts/harness-decode.ts +136 -0
  74. package/fuzz/ts/harness-verify.ts +121 -0
  75. package/fuzz/ts/run.ts +35 -0
  76. package/mcp-server/README.md +38 -0
  77. package/mcp-server/package-lock.json +1187 -0
  78. package/mcp-server/package.json +35 -0
  79. package/mcp-server/src/index.ts +236 -0
  80. package/mcp-server/tsconfig.json +14 -0
  81. package/package.json +79 -13
  82. package/scripts/aip-framework-quickstart.ts +110 -0
  83. package/scripts/aip-mcp-gateway.ts +38 -0
  84. package/scripts/aip-mcp-wrap-demo.ts +72 -0
  85. package/scripts/aip-quickstart.ts +60 -0
  86. package/scripts/bench-diff.ts +118 -0
  87. package/scripts/bench-protocol-e2e.ts +937 -0
  88. package/scripts/bench-protocol-openloop.ts +1397 -0
  89. package/scripts/bench-replay-cache.ts +76 -0
  90. package/scripts/bench-signature-profiles.ts +180 -0
  91. package/scripts/bench-wire-codecs.ts +161 -0
  92. package/scripts/build-binary-conformance.ts +36 -0
  93. package/scripts/build-release-dashboard.ts +175 -0
  94. package/scripts/canary-rollout.ts +38 -0
  95. package/scripts/mcpGatewayCli.test.ts +116 -0
  96. package/scripts/prepare-aip-package.ts +88 -0
  97. package/scripts/regen-conformance-sigs.ts +18 -0
  98. package/scripts/release-gate.ts +19 -0
  99. package/scripts/validate-runtime-policy.ts +18 -0
  100. package/sdk/browser/index.test.ts +162 -0
  101. package/sdk/browser/index.ts +257 -0
  102. package/sdk/browser/package.json +13 -0
  103. package/sdk/go/go.mod +3 -0
  104. package/sdk/go/http.go +135 -0
  105. package/sdk/go/protocol.go +324 -0
  106. package/sdk/go/protocol_test.go +334 -0
  107. package/sdk/go/webhook.go +136 -0
  108. package/sdk/python/README.md +18 -0
  109. package/sdk/python/protocol_7h3/__init__.py +46 -0
  110. package/sdk/python/protocol_7h3/http.py +212 -0
  111. package/sdk/python/protocol_7h3/keys.py +149 -0
  112. package/sdk/python/protocol_7h3/protocol.py +525 -0
  113. package/sdk/python/protocol_7h3/queue.py +118 -0
  114. package/sdk/python/protocol_7h3/webhook.py +116 -0
  115. package/sdk/python/pyproject.toml +40 -0
  116. package/sdk/python/tests/test_conformance.py +110 -0
  117. package/sdk/python/tests/test_http.py +305 -0
  118. package/sdk/python/tests/test_keys.py +417 -0
  119. package/sdk/python/tests/test_queue.py +120 -0
  120. package/sdk/python/tests/test_webhook.py +345 -0
  121. package/sdk/rust/Cargo.lock +371 -0
  122. package/sdk/rust/Cargo.toml +25 -0
  123. package/sdk/rust/README.md +31 -0
  124. package/sdk/rust/fuzz/Cargo.toml +29 -0
  125. package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +46 -0
  126. package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +11 -0
  127. package/sdk/rust/src/bin/aip_mcp_gateway.rs +59 -0
  128. package/sdk/rust/src/http.rs +145 -0
  129. package/sdk/rust/src/keys.rs +161 -0
  130. package/sdk/rust/src/lib.rs +688 -0
  131. package/sdk/rust/src/queue.rs +79 -0
  132. package/sdk/rust/src/webhook.rs +86 -0
  133. package/sdk/rust/tests/conformance.rs +148 -0
  134. package/sdk/rust/tests/gateway.rs +130 -0
  135. package/sdk/rust/tests/http_webhook_queue.rs +201 -0
  136. package/sdk/rust/tests/keys.rs +189 -0
  137. package/src/agentAdapter.test.ts +48 -0
  138. package/src/agentAdapter.ts +56 -0
  139. package/src/auditLog.test.ts +145 -0
  140. package/src/auditLog.ts +147 -0
  141. package/src/conformance.test.ts +136 -0
  142. package/src/conformanceVectors.ts +99 -0
  143. package/src/frameworkAdapters.test.ts +290 -0
  144. package/src/frameworkAdapters.ts +261 -0
  145. package/src/gateway.test.ts +343 -0
  146. package/src/gateway.ts +171 -0
  147. package/src/grpcBinding.test.ts +211 -0
  148. package/src/grpcBinding.ts +103 -0
  149. package/src/httpBinding.test.ts +376 -0
  150. package/src/httpBinding.ts +163 -0
  151. package/src/index.ts +32 -0
  152. package/src/keyInfra.test.ts +278 -0
  153. package/src/keyInfra.ts +228 -0
  154. package/src/keyRegistry.ts +59 -0
  155. package/src/keyRotation.test.ts +78 -0
  156. package/src/keyRotation.ts +72 -0
  157. package/src/mcpGateway.test.ts +129 -0
  158. package/src/mcpGateway.ts +250 -0
  159. package/src/mcpTransports.test.ts +92 -0
  160. package/src/mcpTransports.ts +169 -0
  161. package/src/mcpWrapper.test.ts +179 -0
  162. package/src/mcpWrapper.ts +206 -0
  163. package/src/policyEnforcer.test.ts +99 -0
  164. package/src/policyEnforcer.ts +169 -0
  165. package/src/policyTelemetryFeedback.test.ts +25 -0
  166. package/src/policyTelemetryFeedback.ts +38 -0
  167. package/src/protocol.bench.ts +37 -0
  168. package/src/protocol.test.ts +155 -0
  169. package/src/protocol.ts +413 -0
  170. package/src/protocolAgent.test.ts +105 -0
  171. package/src/protocolAgent.ts +169 -0
  172. package/src/protocolBinary.test.ts +165 -0
  173. package/src/protocolBinary.ts +312 -0
  174. package/src/protocolCapabilities.ts +70 -0
  175. package/src/protocolFuzz.advanced.test.ts +235 -0
  176. package/src/protocolFuzz.test.ts +111 -0
  177. package/src/protocolNegative.test.ts +97 -0
  178. package/src/protocolReplay.test.ts +71 -0
  179. package/src/protocolReplay.ts +194 -0
  180. package/src/protocolTransport.test.ts +556 -0
  181. package/src/protocolTransport.ts +483 -0
  182. package/src/queueBinding.test.ts +130 -0
  183. package/src/queueBinding.ts +102 -0
  184. package/src/rateLimiter.test.ts +96 -0
  185. package/src/rateLimiter.ts +46 -0
  186. package/src/redisClient.ts +140 -0
  187. package/src/redisIntegration.test.ts +134 -0
  188. package/src/replayStores.test.ts +141 -0
  189. package/src/replayStores.ts +82 -0
  190. package/src/revocation.test.ts +98 -0
  191. package/src/revocation.ts +0 -0
  192. package/src/routePolicy.test.ts +87 -0
  193. package/src/routePolicy.ts +72 -0
  194. package/src/runtimePolicy.test.ts +49 -0
  195. package/src/runtimePolicy.ts +81 -0
  196. package/src/runtimePolicyManager.test.ts +29 -0
  197. package/src/runtimePolicyManager.ts +50 -0
  198. package/src/runtimePolicyPresets.ts +43 -0
  199. package/src/signedResponse.test.ts +111 -0
  200. package/src/signedResponse.ts +83 -0
  201. package/src/webhookBinding.test.ts +144 -0
  202. package/src/webhookBinding.ts +115 -0
  203. package/src/wsBinding.test.ts +221 -0
  204. package/src/wsBinding.ts +100 -0
  205. package/tsconfig.json +15 -0
  206. package/tsconfig.lib.json +23 -0
  207. package/vite.lib.config.ts +16 -0
  208. package/agentAdapter.d.ts +0 -26
  209. package/conformanceVectors.d.ts +0 -20
  210. package/frameworkAdapters.d.ts +0 -72
  211. package/index.d.ts +0 -20
  212. package/index.js +0 -1702
  213. package/keyRotation.d.ts +0 -20
  214. package/mcpGateway.d.ts +0 -37
  215. package/mcpTransports.d.ts +0 -62
  216. package/mcpWrapper.d.ts +0 -83
  217. package/policyEnforcer.d.ts +0 -50
  218. package/policyTelemetryFeedback.d.ts +0 -11
  219. package/protocol.d.ts +0 -66
  220. package/protocolAgent.d.ts +0 -58
  221. package/protocolBinary.d.ts +0 -8
  222. package/protocolCapabilities.d.ts +0 -24
  223. package/protocolReplay.d.ts +0 -35
  224. package/protocolTransport.d.ts +0 -73
  225. package/redisClient.d.ts +0 -49
  226. package/replayStores.d.ts +0 -32
  227. package/revocation.d.ts +0 -71
  228. package/runtimePolicy.d.ts +0 -24
  229. package/runtimePolicyManager.d.ts +0 -15
  230. package/runtimePolicyPresets.d.ts +0 -11
@@ -0,0 +1,189 @@
1
+ use protocol_7h3::keys::{
2
+ KeyEntry, KeyRotationManager, ManagedKeyPair, RevocationRegistry, WellKnownKeysDocument,
3
+ };
4
+ use std::time::{SystemTime, UNIX_EPOCH};
5
+
6
+ fn now_ms() -> u64 {
7
+ SystemTime::now()
8
+ .duration_since(UNIX_EPOCH)
9
+ .unwrap_or_default()
10
+ .as_millis() as u64
11
+ }
12
+
13
+ fn make_key_entry(id: &str, revoked: Option<bool>, expires: Option<u64>) -> KeyEntry {
14
+ KeyEntry {
15
+ id: id.to_string(),
16
+ algorithm: "Ed25519".to_string(),
17
+ public_key: "dGVzdC1wdWJsaWMta2V5".to_string(),
18
+ created: now_ms() - 1000,
19
+ expires,
20
+ revoked,
21
+ revoked_at: None,
22
+ }
23
+ }
24
+
25
+ // 1. WellKnownKeysDocument round-trip JSON serialization
26
+ #[test]
27
+ fn test_well_known_document_json_round_trip() {
28
+ let entry = make_key_entry("key-1", None, None);
29
+ let doc = WellKnownKeysDocument::new(vec![entry]);
30
+
31
+ let json = doc.to_json().expect("serialization should succeed");
32
+ let restored = WellKnownKeysDocument::from_json(&json).expect("deserialization should succeed");
33
+
34
+ assert_eq!(restored.version, "7h3/0.1");
35
+ assert_eq!(restored.keys.len(), 1);
36
+ assert_eq!(restored.keys[0].id, "key-1");
37
+ assert_eq!(restored.keys[0].algorithm, "Ed25519");
38
+ }
39
+
40
+ // 2. get_active_key finds non-revoked keys
41
+ #[test]
42
+ fn test_get_active_key_finds_active() {
43
+ let entry = make_key_entry("key-active", None, None);
44
+ let doc = WellKnownKeysDocument::new(vec![entry]);
45
+
46
+ let found = doc.get_active_key("key-active");
47
+ assert!(found.is_some(), "should find an active key");
48
+ assert_eq!(found.unwrap().id, "key-active");
49
+ }
50
+
51
+ // 3. get_active_key ignores revoked keys
52
+ #[test]
53
+ fn test_get_active_key_ignores_revoked() {
54
+ let revoked = make_key_entry("key-revoked", Some(true), None);
55
+ let doc = WellKnownKeysDocument::new(vec![revoked]);
56
+
57
+ let found = doc.get_active_key("key-revoked");
58
+ assert!(found.is_none(), "should NOT find a revoked key");
59
+ }
60
+
61
+ // 3b. get_active_key ignores expired keys
62
+ #[test]
63
+ fn test_get_active_key_ignores_expired() {
64
+ // expires 10 seconds in the past
65
+ let expired = make_key_entry("key-expired", None, Some(now_ms() - 10_000));
66
+ let doc = WellKnownKeysDocument::new(vec![expired]);
67
+
68
+ let found = doc.get_active_key("key-expired");
69
+ assert!(found.is_none(), "should NOT find an expired key");
70
+ }
71
+
72
+ // 4. KeyRotationManager: add_key + get_current_key
73
+ #[test]
74
+ fn test_rotation_manager_add_and_get_current() {
75
+ let mgr = KeyRotationManager::new(86_400_000, 3_600_000);
76
+
77
+ let pair = ManagedKeyPair {
78
+ id: "rot-key-1".to_string(),
79
+ public_key: "pub1".to_string(),
80
+ private_key: "priv1".to_string(),
81
+ created: now_ms(),
82
+ expires_at: None,
83
+ };
84
+ mgr.add_key(pair);
85
+
86
+ let current = mgr.get_current_key();
87
+ assert!(current.is_some(), "should return current key");
88
+ assert_eq!(current.unwrap().id, "rot-key-1");
89
+ }
90
+
91
+ // 4b. get_current_key returns the most recent non-expired key
92
+ #[test]
93
+ fn test_rotation_manager_returns_most_recent() {
94
+ let mgr = KeyRotationManager::new(86_400_000, 3_600_000);
95
+ let base = now_ms();
96
+
97
+ mgr.add_key(ManagedKeyPair {
98
+ id: "older".to_string(),
99
+ public_key: "pub-older".to_string(),
100
+ private_key: "priv-older".to_string(),
101
+ created: base - 5000,
102
+ expires_at: None,
103
+ });
104
+ mgr.add_key(ManagedKeyPair {
105
+ id: "newer".to_string(),
106
+ public_key: "pub-newer".to_string(),
107
+ private_key: "priv-newer".to_string(),
108
+ created: base,
109
+ expires_at: None,
110
+ });
111
+
112
+ let current = mgr.get_current_key().expect("should have a current key");
113
+ assert_eq!(current.id, "newer");
114
+ }
115
+
116
+ // 4c. get_current_key excludes expired keys
117
+ #[test]
118
+ fn test_rotation_manager_excludes_expired() {
119
+ let mgr = KeyRotationManager::new(86_400_000, 3_600_000);
120
+
121
+ mgr.add_key(ManagedKeyPair {
122
+ id: "exp-key".to_string(),
123
+ public_key: "pub-exp".to_string(),
124
+ private_key: "priv-exp".to_string(),
125
+ created: now_ms() - 2000,
126
+ expires_at: Some(now_ms() - 1000), // already expired
127
+ });
128
+
129
+ let current = mgr.get_current_key();
130
+ assert!(current.is_none(), "expired key should not be returned as current");
131
+ }
132
+
133
+ // 5. KeyRotationManager: get_well_known_document structure
134
+ #[test]
135
+ fn test_rotation_manager_well_known_document() {
136
+ let mgr = KeyRotationManager::new(86_400_000, 3_600_000);
137
+
138
+ mgr.add_key(ManagedKeyPair {
139
+ id: "doc-key-1".to_string(),
140
+ public_key: "pub-doc".to_string(),
141
+ private_key: "priv-doc".to_string(),
142
+ created: now_ms(),
143
+ expires_at: None,
144
+ });
145
+
146
+ let doc = mgr.get_well_known_document();
147
+ assert_eq!(doc.version, "7h3/0.1");
148
+ assert_eq!(doc.keys.len(), 1);
149
+ assert_eq!(doc.keys[0].id, "doc-key-1");
150
+ assert_eq!(doc.keys[0].algorithm, "Ed25519");
151
+ assert_eq!(doc.keys[0].public_key, "pub-doc");
152
+ // non-expired key should not be marked revoked
153
+ assert!(doc.keys[0].revoked.is_none() || !doc.keys[0].revoked.unwrap());
154
+ }
155
+
156
+ // 6. RevocationRegistry: revoke + is_revoked
157
+ #[test]
158
+ fn test_revocation_registry_revoke_and_check() {
159
+ let registry = RevocationRegistry::new();
160
+
161
+ assert!(!registry.is_revoked("key-x"), "should not be revoked initially");
162
+ registry.revoke("key-x", Some("compromised"));
163
+ assert!(registry.is_revoked("key-x"), "should be revoked after revoke()");
164
+ assert!(!registry.is_revoked("key-y"), "unrelated key should not be revoked");
165
+ }
166
+
167
+ // 7. RevocationRegistry: get_list contains correct fields
168
+ #[test]
169
+ fn test_revocation_registry_get_list() {
170
+ let registry = RevocationRegistry::new();
171
+ registry.revoke("key-a", Some("expired"));
172
+ registry.revoke("key-b", None);
173
+
174
+ let list = registry.get_list();
175
+
176
+ assert_eq!(list["version"], "7h3/0.1");
177
+ let revoked_keys = list["revokedKeys"].as_array().expect("revokedKeys should be array");
178
+ assert_eq!(revoked_keys.len(), 2);
179
+
180
+ // Find key-a in the list
181
+ let key_a = revoked_keys.iter().find(|e| e["id"] == "key-a").expect("key-a should be in list");
182
+ assert!(key_a["revokedAt"].as_u64().unwrap() > 0, "revokedAt should be a positive timestamp");
183
+ assert_eq!(key_a["reason"], "expired");
184
+
185
+ // Find key-b in the list — no reason
186
+ let key_b = revoked_keys.iter().find(|e| e["id"] == "key-b").expect("key-b should be in list");
187
+ assert!(key_b["revokedAt"].as_u64().unwrap() > 0);
188
+ assert!(key_b.get("reason").is_none() || key_b["reason"].is_null());
189
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createAipAgentAdapter } from './agentAdapter'
3
+ import type { WireEnvelope } from './protocolTransport'
4
+
5
+ describe('AIP agent adapter', () => {
6
+ it('supports raw task request/response flow', async () => {
7
+ const secret = 'adapter-shared-secret'
8
+
9
+ const coordinator = createAipAgentAdapter({
10
+ agentId: 'agent.coordinator',
11
+ outboundSecret: secret,
12
+ sharedSecrets: { 'agent.worker': secret },
13
+ wireFormat: 'compact',
14
+ })
15
+
16
+ const worker = createAipAgentAdapter({
17
+ agentId: 'agent.worker',
18
+ outboundSecret: secret,
19
+ sharedSecrets: { 'agent.coordinator': secret },
20
+ wireFormat: 'compact',
21
+ onTask: async (envelope) => ({
22
+ intent: 'RESULT',
23
+ content: `handled:${envelope.body.content}`,
24
+ capability: envelope.body.capability,
25
+ }),
26
+ })
27
+
28
+ const taskRaw = await coordinator.createRawIntent({
29
+ recipient: 'agent.worker',
30
+ intent: 'TASK',
31
+ content: 'optimize route',
32
+ capability: 'task.plan',
33
+ })
34
+
35
+ let responseRaw: WireEnvelope = ''
36
+ const handled = await worker.handleRaw(taskRaw, async (rawResponse) => {
37
+ responseRaw = rawResponse
38
+ })
39
+
40
+ expect(handled.ok).toBe(true)
41
+ expect(responseRaw.length).toBeGreaterThan(0)
42
+
43
+ const verification = await coordinator.receiveRaw(responseRaw)
44
+ expect(verification.ok).toBe(true)
45
+ expect(verification.received?.body.intent).toBe('RESULT')
46
+ expect(verification.received?.body.content).toContain('handled:optimize route')
47
+ })
48
+ })
@@ -0,0 +1,56 @@
1
+ import { type AgentReceiveResult, type AgentSessionConfig, AgentSession } from './protocolAgent'
2
+ import { type IntentKind, type ProtocolEnvelope } from './protocol'
3
+ import { encodeEnvelope, type WireEnvelope, type WireFormat } from './protocolTransport'
4
+
5
+ export interface AipAdapterConfig extends AgentSessionConfig {
6
+ wireFormat?: WireFormat
7
+ }
8
+
9
+ export class AipAgentAdapter {
10
+ private readonly session: AgentSession
11
+ private readonly wireFormat: WireFormat
12
+
13
+ constructor(config: AipAdapterConfig) {
14
+ this.session = new AgentSession(config)
15
+ this.wireFormat = config.wireFormat ?? 'compact'
16
+ }
17
+
18
+ toRaw(envelope: ProtocolEnvelope): WireEnvelope {
19
+ return encodeEnvelope(envelope, this.wireFormat)
20
+ }
21
+
22
+ async createRawIntent(input: {
23
+ recipient?: string
24
+ intent: IntentKind
25
+ content: string
26
+ capability?: string
27
+ correlationId?: string
28
+ ttlMs?: number
29
+ messageId?: string
30
+ nonce?: string
31
+ nowMs?: number
32
+ }): Promise<WireEnvelope> {
33
+ const envelope = await this.session.createSignedIntent(input)
34
+ return this.toRaw(envelope)
35
+ }
36
+
37
+ async receiveRaw(raw: WireEnvelope, nowMs = Date.now()): Promise<AgentReceiveResult> {
38
+ return this.session.receiveAndRespond(raw, nowMs)
39
+ }
40
+
41
+ async handleRaw(
42
+ raw: WireEnvelope,
43
+ emitResponseRaw: (rawResponse: WireEnvelope, responseEnvelope: ProtocolEnvelope) => void | Promise<void>,
44
+ nowMs = Date.now(),
45
+ ): Promise<AgentReceiveResult> {
46
+ const result = await this.session.receiveAndRespond(raw, nowMs)
47
+ if (result.response) {
48
+ await emitResponseRaw(this.toRaw(result.response), result.response)
49
+ }
50
+ return result
51
+ }
52
+ }
53
+
54
+ export function createAipAgentAdapter(config: AipAdapterConfig): AipAgentAdapter {
55
+ return new AipAgentAdapter(config)
56
+ }
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest'
2
+ import { createAuditLog, NoopAuditLog, type AuditEntry } from './auditLog'
3
+ import { generateEd25519KeypairBase64Url } from './protocol'
4
+
5
+ let keys: { publicKey: string; privateKey: string }
6
+
7
+ beforeAll(async () => {
8
+ keys = await generateEd25519KeypairBase64Url()
9
+ })
10
+
11
+ describe('InMemoryAuditLog', () => {
12
+ it('logs and queries entries', async () => {
13
+ const log = createAuditLog(keys.privateKey)
14
+ await log.log({ type: 'verify-ok', sender: 'agent-a', path: '/api/test', method: 'GET' })
15
+ await log.log({ type: 'verify-fail', sender: 'agent-b', path: '/api/test', method: 'POST', failReason: 'invalid-signature' })
16
+
17
+ const all = await log.query()
18
+ expect(all).toHaveLength(2)
19
+ expect(log.size()).toBe(2)
20
+ })
21
+
22
+ it('query filters by type', async () => {
23
+ const log = createAuditLog(keys.privateKey)
24
+ await log.log({ type: 'verify-ok', sender: 'alice' })
25
+ await log.log({ type: 'verify-fail', sender: 'bob', failReason: 'expired' })
26
+ await log.log({ type: 'rate-limited', sender: 'alice' })
27
+
28
+ const oks = await log.query({ type: 'verify-ok' })
29
+ expect(oks).toHaveLength(1)
30
+ expect(oks[0].type).toBe('verify-ok')
31
+ })
32
+
33
+ it('query filters by sender', async () => {
34
+ const log = createAuditLog(keys.privateKey)
35
+ await log.log({ type: 'verify-ok', sender: 'alice' })
36
+ await log.log({ type: 'verify-ok', sender: 'bob' })
37
+ await log.log({ type: 'verify-fail', sender: 'alice', failReason: 'expired' })
38
+
39
+ const aliceEntries = await log.query({ sender: 'alice' })
40
+ expect(aliceEntries).toHaveLength(2)
41
+ expect(aliceEntries.every(e => e.sender === 'alice')).toBe(true)
42
+ })
43
+
44
+ it('query filters by since', async () => {
45
+ const log = createAuditLog(keys.privateKey)
46
+ const before = Date.now()
47
+ await log.log({ type: 'verify-ok', sender: 'alice' })
48
+ await new Promise(r => setTimeout(r, 5))
49
+ const mid = Date.now()
50
+ await log.log({ type: 'verify-ok', sender: 'bob' })
51
+
52
+ const recent = await log.query({ since: mid })
53
+ expect(recent).toHaveLength(1)
54
+ expect(recent[0].sender).toBe('bob')
55
+
56
+ const all = await log.query({ since: before - 1 })
57
+ expect(all).toHaveLength(2)
58
+ })
59
+
60
+ it('query respects limit', async () => {
61
+ const log = createAuditLog(keys.privateKey)
62
+ for (let i = 0; i < 5; i++) {
63
+ await log.log({ type: 'verify-ok', sender: `agent-${i}` })
64
+ }
65
+ const limited = await log.query({ limit: 2 })
66
+ expect(limited).toHaveLength(2)
67
+ })
68
+
69
+ it('verify() returns true for valid entry', async () => {
70
+ const log = createAuditLog(keys.privateKey)
71
+ await log.log({ type: 'verify-ok', sender: 'alice', path: '/api/test' })
72
+
73
+ const entries = await log.query()
74
+ expect(entries).toHaveLength(1)
75
+ const valid = await log.verify(entries[0], keys.publicKey)
76
+ expect(valid).toBe(true)
77
+ })
78
+
79
+ it('verify() returns false for tampered entry', async () => {
80
+ const log = createAuditLog(keys.privateKey)
81
+ await log.log({ type: 'verify-ok', sender: 'alice', path: '/api/test' })
82
+
83
+ const entries = await log.query()
84
+ const tampered: AuditEntry = { ...entries[0], sender: 'eve' }
85
+ const valid = await log.verify(tampered, keys.publicKey)
86
+ expect(valid).toBe(false)
87
+ })
88
+
89
+ it('verify() returns false with wrong public key', async () => {
90
+ const log = createAuditLog(keys.privateKey)
91
+ await log.log({ type: 'verify-ok', sender: 'alice' })
92
+
93
+ const wrongKeys = await generateEd25519KeypairBase64Url()
94
+ const entries = await log.query()
95
+ const valid = await log.verify(entries[0], wrongKeys.publicKey)
96
+ expect(valid).toBe(false)
97
+ })
98
+
99
+ it('respects maxEntries by evicting oldest', async () => {
100
+ const log = createAuditLog(keys.privateKey, { maxEntries: 3 })
101
+ for (let i = 0; i < 5; i++) {
102
+ await log.log({ type: 'verify-ok', sender: `agent-${i}` })
103
+ }
104
+ expect(log.size()).toBe(3)
105
+ const entries = await log.query()
106
+ // Should have the last 3 (agents 2, 3, 4)
107
+ expect(entries[0].sender).toBe('agent-2')
108
+ })
109
+
110
+ it('entries have id, timestampMs, and entrySignature', async () => {
111
+ const log = createAuditLog(keys.privateKey)
112
+ await log.log({ type: 'response-signed', upstream: 'http://upstream' })
113
+
114
+ const entries = await log.query()
115
+ expect(typeof entries[0].id).toBe('string')
116
+ expect(typeof entries[0].timestampMs).toBe('number')
117
+ expect(typeof entries[0].entrySignature).toBe('string')
118
+ expect(entries[0].entrySignature.length).toBeGreaterThan(0)
119
+ })
120
+ })
121
+
122
+ describe('NoopAuditLog', () => {
123
+ it('log does nothing', async () => {
124
+ const log = new NoopAuditLog()
125
+ await log.log({ type: 'verify-ok', sender: 'alice' })
126
+ expect(log.size()).toBe(0)
127
+ })
128
+
129
+ it('query returns empty array', async () => {
130
+ const log = new NoopAuditLog()
131
+ const result = await log.query()
132
+ expect(result).toEqual([])
133
+ })
134
+
135
+ it('verify always returns false', async () => {
136
+ const log = new NoopAuditLog()
137
+ const fakeEntry = {
138
+ id: 'x',
139
+ timestampMs: Date.now(),
140
+ type: 'verify-ok' as const,
141
+ entrySignature: 'fake',
142
+ }
143
+ expect(await log.verify(fakeEntry, 'key')).toBe(false)
144
+ })
145
+ })
@@ -0,0 +1,147 @@
1
+ import {
2
+ signCanonicalPayloadEd25519,
3
+ verifyCanonicalPayloadEd25519,
4
+ } from './protocol'
5
+
6
+ export type AuditEventType =
7
+ | 'verify-ok'
8
+ | 'verify-fail'
9
+ | 'rate-limited'
10
+ | 'sender-denied'
11
+ | 'response-signed'
12
+
13
+ export interface AuditEntry {
14
+ id: string
15
+ timestampMs: number
16
+ type: AuditEventType
17
+ sender?: string
18
+ path?: string
19
+ method?: string
20
+ envelopeId?: string
21
+ failReason?: string
22
+ upstream?: string
23
+ responseStatus?: number
24
+ entrySignature: string // Ed25519 sig over the entry (minus entrySignature itself)
25
+ }
26
+
27
+ /**
28
+ * Produce a canonical payload string from an AuditEntry, excluding entrySignature.
29
+ * Uses explicit key order and omits undefined fields for determinism.
30
+ */
31
+ function canonicalAuditPayload(entry: Omit<AuditEntry, 'entrySignature'> & { entrySignature?: string }): string {
32
+ const parts: string[] = [
33
+ `"id":${JSON.stringify(entry.id)}`,
34
+ `"timestampMs":${entry.timestampMs}`,
35
+ `"type":${JSON.stringify(entry.type)}`,
36
+ ]
37
+ if (entry.sender !== undefined) parts.push(`"sender":${JSON.stringify(entry.sender)}`)
38
+ if (entry.path !== undefined) parts.push(`"path":${JSON.stringify(entry.path)}`)
39
+ if (entry.method !== undefined) parts.push(`"method":${JSON.stringify(entry.method)}`)
40
+ if (entry.envelopeId !== undefined) parts.push(`"envelopeId":${JSON.stringify(entry.envelopeId)}`)
41
+ if (entry.failReason !== undefined) parts.push(`"failReason":${JSON.stringify(entry.failReason)}`)
42
+ if (entry.upstream !== undefined) parts.push(`"upstream":${JSON.stringify(entry.upstream)}`)
43
+ if (entry.responseStatus !== undefined) parts.push(`"responseStatus":${entry.responseStatus}`)
44
+ return `{${parts.join(',')}}`
45
+ }
46
+
47
+ export interface AuditLogger {
48
+ log(event: Omit<AuditEntry, 'id' | 'timestampMs' | 'entrySignature'>): Promise<void>
49
+ query(opts?: { type?: AuditEventType; sender?: string; since?: number; limit?: number }): Promise<AuditEntry[]>
50
+ verify(entry: AuditEntry, publicKey: string): Promise<boolean>
51
+ }
52
+
53
+ class InMemoryAuditLog implements AuditLogger {
54
+ private entries: AuditEntry[] = []
55
+ private privateKey: string
56
+ private maxEntries: number
57
+
58
+ constructor(privateKey: string, maxEntries = 10_000) {
59
+ this.privateKey = privateKey
60
+ this.maxEntries = maxEntries
61
+ }
62
+
63
+ async log(event: Omit<AuditEntry, 'id' | 'timestampMs' | 'entrySignature'>): Promise<void> {
64
+ const id = `audit-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
65
+ const timestampMs = Date.now()
66
+
67
+ const partial: Omit<AuditEntry, 'entrySignature'> = { id, timestampMs, ...event }
68
+ const payload = canonicalAuditPayload(partial)
69
+ const entrySignature = await signCanonicalPayloadEd25519(payload, this.privateKey)
70
+
71
+ const entry: AuditEntry = { ...partial, entrySignature }
72
+
73
+ if (this.entries.length >= this.maxEntries) {
74
+ this.entries.shift()
75
+ }
76
+ this.entries.push(entry)
77
+ }
78
+
79
+ async query(opts?: {
80
+ type?: AuditEventType
81
+ sender?: string
82
+ since?: number
83
+ limit?: number
84
+ }): Promise<AuditEntry[]> {
85
+ let results = [...this.entries]
86
+
87
+ if (opts?.type !== undefined) {
88
+ results = results.filter(e => e.type === opts.type)
89
+ }
90
+ if (opts?.sender !== undefined) {
91
+ results = results.filter(e => e.sender === opts.sender)
92
+ }
93
+ if (opts?.since !== undefined) {
94
+ results = results.filter(e => e.timestampMs >= opts.since!)
95
+ }
96
+ if (opts?.limit !== undefined) {
97
+ results = results.slice(-opts.limit)
98
+ }
99
+
100
+ return results
101
+ }
102
+
103
+ async verify(entry: AuditEntry, publicKey: string): Promise<boolean> {
104
+ const partial: Omit<AuditEntry, 'entrySignature'> = {
105
+ id: entry.id,
106
+ timestampMs: entry.timestampMs,
107
+ type: entry.type,
108
+ sender: entry.sender,
109
+ path: entry.path,
110
+ method: entry.method,
111
+ envelopeId: entry.envelopeId,
112
+ failReason: entry.failReason,
113
+ upstream: entry.upstream,
114
+ responseStatus: entry.responseStatus,
115
+ }
116
+ const payload = canonicalAuditPayload(partial)
117
+ return verifyCanonicalPayloadEd25519(payload, entry.entrySignature, publicKey)
118
+ }
119
+
120
+ size(): number {
121
+ return this.entries.length
122
+ }
123
+ }
124
+
125
+ class NoopAuditLog implements AuditLogger {
126
+ async log(_event: Omit<AuditEntry, 'id' | 'timestampMs' | 'entrySignature'>): Promise<void> {}
127
+ async query(_opts?: {
128
+ type?: AuditEventType
129
+ sender?: string
130
+ since?: number
131
+ limit?: number
132
+ }): Promise<AuditEntry[]> {
133
+ return []
134
+ }
135
+ async verify(_entry: AuditEntry, _publicKey: string): Promise<boolean> {
136
+ return false
137
+ }
138
+ size(): number {
139
+ return 0
140
+ }
141
+ }
142
+
143
+ export function createAuditLog(privateKey: string, opts?: { maxEntries?: number }): InMemoryAuditLog {
144
+ return new InMemoryAuditLog(privateKey, opts?.maxEntries)
145
+ }
146
+
147
+ export { InMemoryAuditLog, NoopAuditLog }