@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,937 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import http2 from 'node:http2'
3
+ import { performance } from 'node:perf_hooks'
4
+ import process from 'node:process'
5
+ import { WebSocket, WebSocketServer } from 'ws'
6
+ import {
7
+ canonicalizeEnvelope,
8
+ signCanonicalPayloadHmac,
9
+ type ProtocolEnvelope,
10
+ type ProtocolHeader,
11
+ } from '../src/protocol'
12
+ import { InMemoryReplayCache } from '../src/protocolReplay'
13
+ import { InMemoryVerificationMaterialCache, decodeEnvelope, encodeEnvelope, receiveEnvelope } from '../src/protocolTransport'
14
+
15
+ type ProfileName = 'quick' | 'full'
16
+ type Mode = 'inproc' | 'http' | 'ws' | 'agent-loop'
17
+
18
+ interface Profile {
19
+ warmupMs: number
20
+ measureMs: number
21
+ }
22
+
23
+ interface Scenario {
24
+ mode: Mode
25
+ payloadBytes: number
26
+ concurrency: number
27
+ }
28
+
29
+ interface StageTotals {
30
+ buildMs: number
31
+ canonicalizeMs: number
32
+ macMs: number
33
+ queueMs: number
34
+ transportMs: number
35
+ verifyMs: number
36
+ decodeMs: number
37
+ }
38
+
39
+ interface ScenarioResult {
40
+ mode: Mode
41
+ payloadBytes: number
42
+ concurrency: number
43
+ ops: number
44
+ opsPerSecond: number
45
+ p50Ms: number
46
+ p95Ms: number
47
+ p99Ms: number
48
+ cpuMsPer10kOps: number
49
+ heapDeltaMb: number
50
+ rssDeltaMb: number
51
+ allocBytesPerOp: number
52
+ stageBuildMsPerOp: number
53
+ stageCanonicalizeMsPerOp: number
54
+ stageMacMsPerOp: number
55
+ stageQueueMsPerOp: number
56
+ stageTransportMsPerOp: number
57
+ stageVerifyMsPerOp: number
58
+ stageDecodeMsPerOp: number
59
+ }
60
+
61
+ interface TransportTrace {
62
+ queueMs: number
63
+ transportMs: number
64
+ verifyMs: number
65
+ decodeMs: number
66
+ }
67
+
68
+ interface TransportAdapter {
69
+ start(): Promise<void>
70
+ send(rawEnvelope: string): Promise<TransportTrace>
71
+ stop(): Promise<void>
72
+ }
73
+
74
+ interface BenchEnvelopeTemplate {
75
+ headerBase: Omit<ProtocolHeader, 'messageId' | 'timestampMs' | 'nonce'>
76
+ intent: 'TASK'
77
+ capability: string
78
+ }
79
+
80
+ interface ServerAck {
81
+ ok: boolean
82
+ queueMs: number
83
+ verifyMs: number
84
+ decodeMs: number
85
+ error?: string
86
+ }
87
+
88
+ const ACK_HEADER_OK = 'x-aip-ok'
89
+ const ACK_HEADER_QUEUE_MS = 'x-aip-queue-ms'
90
+ const ACK_HEADER_VERIFY_MS = 'x-aip-verify-ms'
91
+ const ACK_HEADER_DECODE_MS = 'x-aip-decode-ms'
92
+
93
+ const SHARED_SECRET = 'bench-shared-secret'
94
+ const SHARED_SECRET_RESOLVER = async (): Promise<string> => SHARED_SECRET
95
+ const PROFILES: Record<ProfileName, Profile> = {
96
+ quick: {
97
+ warmupMs: 300,
98
+ measureMs: 1500,
99
+ },
100
+ full: {
101
+ warmupMs: 1200,
102
+ measureMs: 8000,
103
+ },
104
+ }
105
+
106
+ const PAYLOAD_SIZES = [256, 1024, 4096, 16384]
107
+ const CONCURRENCY_LEVELS = [1, 10, 100, 1000]
108
+ const MAX_LATENCY_SAMPLES = 200_000
109
+
110
+ function parseProfile(argv: string[]): ProfileName {
111
+ const idx = argv.indexOf('--profile')
112
+ if (idx >= 0) {
113
+ const value = argv[idx + 1]
114
+ if (value === 'quick' || value === 'full') return value
115
+ }
116
+ return 'quick'
117
+ }
118
+
119
+ function parseCsvArg(flag: string, argv: string[]): string[] {
120
+ const idx = argv.indexOf(flag)
121
+ if (idx < 0) return []
122
+ const raw = argv[idx + 1]
123
+ if (!raw) return []
124
+ return raw
125
+ .split(',')
126
+ .map((part) => part.trim())
127
+ .filter((part) => part.length > 0)
128
+ }
129
+
130
+ function parseModeSelection(argv: string[], fallback: Mode[]): Mode[] {
131
+ const rawModes = parseCsvArg('--modes', argv)
132
+ if (rawModes.length === 0) return fallback
133
+ const allowed = new Set<Mode>(['inproc', 'http', 'ws', 'agent-loop'])
134
+ const parsed = rawModes.filter((raw): raw is Mode => allowed.has(raw as Mode))
135
+ return parsed.length > 0 ? parsed : fallback
136
+ }
137
+
138
+ function parseNumberSelection(argv: string[], flag: string, fallback: number[]): number[] {
139
+ const rawValues = parseCsvArg(flag, argv)
140
+ if (rawValues.length === 0) return fallback
141
+ const parsed = rawValues
142
+ .map((raw) => Number(raw))
143
+ .filter((value) => Number.isFinite(value) && value > 0)
144
+ .map((value) => Math.floor(value))
145
+ return parsed.length > 0 ? parsed : fallback
146
+ }
147
+
148
+ function payloadOfSize(bytes: number): string {
149
+ if (bytes <= 0) return ''
150
+ return 'x'.repeat(bytes)
151
+ }
152
+
153
+ function percentile(sorted: number[], p: number): number {
154
+ if (sorted.length === 0) return 0
155
+ const index = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))
156
+ return sorted[index] ?? 0
157
+ }
158
+
159
+ function round3(value: number): number {
160
+ return Number(value.toFixed(3))
161
+ }
162
+
163
+ function nowMs(): number {
164
+ return Date.now()
165
+ }
166
+
167
+ function maybeGc(): void {
168
+ const withGc = globalThis as unknown as { gc?: () => void }
169
+ withGc.gc?.()
170
+ }
171
+
172
+ function emptyStageTotals(): StageTotals {
173
+ return {
174
+ buildMs: 0,
175
+ canonicalizeMs: 0,
176
+ macMs: 0,
177
+ queueMs: 0,
178
+ transportMs: 0,
179
+ verifyMs: 0,
180
+ decodeMs: 0,
181
+ }
182
+ }
183
+
184
+ function addStageTotals(target: StageTotals, delta: TransportTrace & { buildMs: number; canonicalizeMs: number; macMs: number }): void {
185
+ target.buildMs += delta.buildMs
186
+ target.canonicalizeMs += delta.canonicalizeMs
187
+ target.macMs += delta.macMs
188
+ target.queueMs += delta.queueMs
189
+ target.transportMs += delta.transportMs
190
+ target.verifyMs += delta.verifyMs
191
+ target.decodeMs += delta.decodeMs
192
+ }
193
+
194
+ function buildEnvelope(template: BenchEnvelopeTemplate, payload: string, sequence: number): Omit<ProtocolEnvelope, 'signature'> {
195
+ return {
196
+ header: {
197
+ ...template.headerBase,
198
+ messageId: `m-${sequence}`,
199
+ timestampMs: nowMs(),
200
+ nonce: `n-${sequence}`,
201
+ },
202
+ body: {
203
+ intent: template.intent,
204
+ content: payload,
205
+ capability: template.capability,
206
+ correlationId: `corr-${sequence}`,
207
+ },
208
+ }
209
+ }
210
+
211
+ function parseServerAck(raw: string): ServerAck {
212
+ const parsed = JSON.parse(raw) as Partial<ServerAck>
213
+ return {
214
+ ok: parsed.ok === true,
215
+ queueMs: parsed.queueMs ?? 0,
216
+ verifyMs: parsed.verifyMs ?? 0,
217
+ decodeMs: parsed.decodeMs ?? 0,
218
+ error: parsed.error,
219
+ }
220
+ }
221
+
222
+ class InProcessAdapter implements TransportAdapter {
223
+ private readonly replayCache = new InMemoryReplayCache(1_000_000)
224
+ private readonly verificationMaterialCache = new InMemoryVerificationMaterialCache()
225
+
226
+ async start(): Promise<void> {}
227
+
228
+ async send(rawEnvelope: string): Promise<TransportTrace> {
229
+ const queueStart = performance.now()
230
+ await new Promise<void>((resolve) => setImmediate(resolve))
231
+ const queueMs = performance.now() - queueStart
232
+
233
+ const decodeStart = performance.now()
234
+ const decoded = decodeEnvelope(rawEnvelope)
235
+ const decodeMs = performance.now() - decodeStart
236
+ if (!decoded.ok || !decoded.envelope) {
237
+ throw new Error(decoded.diagnostics.map((d) => d.message).join('; '))
238
+ }
239
+
240
+ const verifyStart = performance.now()
241
+ const result = await receiveEnvelope(decoded.envelope, {
242
+ nowMs: nowMs(),
243
+ replayCache: this.replayCache,
244
+ verificationMaterialCache: this.verificationMaterialCache,
245
+ secretResolver: SHARED_SECRET_RESOLVER,
246
+ })
247
+ const verifyMs = performance.now() - verifyStart
248
+ if (!result.ok) {
249
+ throw new Error(result.diagnostics.map((d) => d.message).join('; '))
250
+ }
251
+
252
+ return {
253
+ queueMs,
254
+ transportMs: 0,
255
+ verifyMs,
256
+ decodeMs,
257
+ }
258
+ }
259
+
260
+ async stop(): Promise<void> {}
261
+ }
262
+
263
+ class HttpAdapter implements TransportAdapter {
264
+ private readonly replayCache = new InMemoryReplayCache(1_000_000)
265
+ private readonly verificationMaterialCache = new InMemoryVerificationMaterialCache()
266
+ private server: http2.Http2Server | null = null
267
+ private client: http2.ClientHttp2Session | null = null
268
+ private port = 0
269
+ private readonly fastAck: boolean
270
+
271
+ constructor(fastAck = true) {
272
+ this.fastAck = fastAck
273
+ }
274
+
275
+ async start(): Promise<void> {
276
+ this.server = http2.createServer({
277
+ settings: {
278
+ maxConcurrentStreams: 4096,
279
+ },
280
+ })
281
+ this.server.on('stream', (stream, headers) => {
282
+ if (headers[':method'] !== 'POST') {
283
+ stream.respond({ ':status': 405 })
284
+ stream.end()
285
+ return
286
+ }
287
+
288
+ const queueStart = performance.now()
289
+ const chunks: Buffer[] = []
290
+ stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
291
+ stream.on('end', async () => {
292
+ const queueMs = performance.now() - queueStart
293
+ const raw = Buffer.concat(chunks).toString('utf8')
294
+
295
+ try {
296
+ const decodeStart = performance.now()
297
+ const decoded = decodeEnvelope(raw)
298
+ const decodeMs = performance.now() - decodeStart
299
+ if (!decoded.ok || !decoded.envelope) {
300
+ stream.respond({ ':status': 400, 'content-type': 'application/json' })
301
+ stream.end(JSON.stringify({ ok: false, queueMs, decodeMs, verifyMs: 0, error: 'decode failed' }))
302
+ return
303
+ }
304
+
305
+ const verifyStart = performance.now()
306
+ const result = await receiveEnvelope(decoded.envelope, {
307
+ nowMs: nowMs(),
308
+ replayCache: this.replayCache,
309
+ verificationMaterialCache: this.verificationMaterialCache,
310
+ secretResolver: SHARED_SECRET_RESOLVER,
311
+ })
312
+ const verifyMs = performance.now() - verifyStart
313
+ const ack: ServerAck = {
314
+ ok: result.ok,
315
+ queueMs,
316
+ decodeMs,
317
+ verifyMs,
318
+ error: result.ok ? undefined : result.diagnostics.map((d) => d.message).join('; '),
319
+ }
320
+
321
+ if (result.ok && this.fastAck) {
322
+ stream.respond({
323
+ ':status': 204,
324
+ [ACK_HEADER_OK]: '1',
325
+ [ACK_HEADER_QUEUE_MS]: String(queueMs),
326
+ [ACK_HEADER_VERIFY_MS]: String(verifyMs),
327
+ [ACK_HEADER_DECODE_MS]: String(decodeMs),
328
+ })
329
+ stream.end()
330
+ return
331
+ }
332
+
333
+ stream.respond({ ':status': result.ok ? 200 : 400, 'content-type': 'application/json' })
334
+ stream.end(JSON.stringify(ack))
335
+ } catch {
336
+ stream.respond({ ':status': 500, 'content-type': 'application/json' })
337
+ stream.end(JSON.stringify({ ok: false, queueMs, decodeMs: 0, verifyMs: 0, error: 'internal http receive error' }))
338
+ }
339
+ })
340
+ })
341
+
342
+ await new Promise<void>((resolve, reject) => {
343
+ this.server?.once('error', reject)
344
+ this.server?.listen(0, '127.0.0.1', () => resolve())
345
+ })
346
+
347
+ const address = this.server.address()
348
+ if (!address || typeof address === 'string') {
349
+ throw new Error('Could not resolve HTTP server address')
350
+ }
351
+ this.port = address.port
352
+
353
+ this.client = http2.connect(`http://127.0.0.1:${this.port}`)
354
+ await new Promise<void>((resolve, reject) => {
355
+ const client = this.client
356
+ if (!client) {
357
+ reject(new Error('Failed to initialize HTTP/2 client'))
358
+ return
359
+ }
360
+ client.once('error', reject)
361
+ client.once('connect', () => {
362
+ client.off('error', reject)
363
+ resolve()
364
+ })
365
+ })
366
+ }
367
+
368
+ async send(rawEnvelope: string): Promise<TransportTrace> {
369
+ const transportStart = performance.now()
370
+
371
+ if (!this.client) {
372
+ throw new Error('HTTP/2 client not started')
373
+ }
374
+
375
+ const response = await new Promise<{ body: string; headers: http2.IncomingHttpHeaders }>((resolve, reject) => {
376
+ const request = this.client!.request({
377
+ ':method': 'POST',
378
+ ':path': '/envelope',
379
+ 'content-type': 'application/json',
380
+ })
381
+
382
+ let statusCode = 500
383
+ let responseHeaders: http2.IncomingHttpHeaders = {}
384
+ const chunks: Buffer[] = []
385
+
386
+ request.on('response', (headers) => {
387
+ const code = headers[':status']
388
+ statusCode = typeof code === 'number' ? code : Number(code ?? 500)
389
+ responseHeaders = headers
390
+ })
391
+ request.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
392
+ request.on('end', () => {
393
+ const body = Buffer.concat(chunks).toString('utf8')
394
+ if (statusCode >= 400) {
395
+ reject(new Error(`HTTP status ${statusCode}: ${body}`))
396
+ return
397
+ }
398
+ resolve({ body, headers: responseHeaders })
399
+ })
400
+
401
+ request.on('error', reject)
402
+ request.end(rawEnvelope)
403
+ })
404
+ const transportMs = performance.now() - transportStart
405
+
406
+ const ack = this.fastAck && readHeaderString(response.headers, ACK_HEADER_OK) === '1' ? ackFromHeaders(response.headers) : parseServerAck(response.body)
407
+ if (!ack.ok) {
408
+ throw new Error(ack.error ?? 'HTTP adapter received non-ok response')
409
+ }
410
+
411
+ return {
412
+ queueMs: ack.queueMs,
413
+ transportMs,
414
+ verifyMs: ack.verifyMs,
415
+ decodeMs: ack.decodeMs,
416
+ }
417
+ }
418
+
419
+ async stop(): Promise<void> {
420
+ if (this.client) {
421
+ await new Promise<void>((resolve) => {
422
+ const client = this.client
423
+ if (!client) {
424
+ resolve()
425
+ return
426
+ }
427
+ client.once('close', () => resolve())
428
+ client.close()
429
+ })
430
+ this.client = null
431
+ }
432
+ if (!this.server) return
433
+ await new Promise<void>((resolve, reject) => {
434
+ this.server?.close((error) => (error ? reject(error) : resolve()))
435
+ })
436
+ this.server = null
437
+ }
438
+ }
439
+
440
+ function readHeaderString(headers: http2.IncomingHttpHeaders, key: string): string | undefined {
441
+ const value = headers[key]
442
+ if (typeof value === 'string') return value
443
+ if (typeof value === 'number') return String(value)
444
+ if (Array.isArray(value)) {
445
+ const first = value[0]
446
+ return typeof first === 'string' ? first : undefined
447
+ }
448
+ return undefined
449
+ }
450
+
451
+ function readHeaderNumber(headers: http2.IncomingHttpHeaders, key: string): number {
452
+ const raw = readHeaderString(headers, key)
453
+ if (!raw) return 0
454
+ const parsed = Number(raw)
455
+ return Number.isFinite(parsed) ? parsed : 0
456
+ }
457
+
458
+ function ackFromHeaders(headers: http2.IncomingHttpHeaders): ServerAck {
459
+ return {
460
+ ok: readHeaderString(headers, ACK_HEADER_OK) === '1',
461
+ queueMs: readHeaderNumber(headers, ACK_HEADER_QUEUE_MS),
462
+ verifyMs: readHeaderNumber(headers, ACK_HEADER_VERIFY_MS),
463
+ decodeMs: readHeaderNumber(headers, ACK_HEADER_DECODE_MS),
464
+ }
465
+ }
466
+
467
+ class WsAdapter implements TransportAdapter {
468
+ private readonly replayCache = new InMemoryReplayCache(1_000_000)
469
+ private readonly verificationMaterialCache = new InMemoryVerificationMaterialCache()
470
+ private server: WebSocketServer | null = null
471
+ private client: WebSocket | null = null
472
+ private nextId = 1
473
+ private readonly pending = new Map<number, { resolve: (trace: TransportTrace) => void; reject: (error: Error) => void }>()
474
+
475
+ async start(): Promise<void> {
476
+ this.server = new WebSocketServer({ port: 0, host: '127.0.0.1' })
477
+
478
+ this.server.on('connection', (socket) => {
479
+ socket.on('message', async (data) => {
480
+ const queueStart = performance.now()
481
+ let message: { id: number; payload: string }
482
+ try {
483
+ message = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data)) as {
484
+ id: number
485
+ payload: string
486
+ }
487
+ } catch {
488
+ socket.send(Buffer.from(JSON.stringify({ id: 0, ok: false, error: 'invalid frame' }), 'utf8'))
489
+ return
490
+ }
491
+
492
+ const queueMs = performance.now() - queueStart
493
+ try {
494
+ const decodeStart = performance.now()
495
+ const decoded = decodeEnvelope(message.payload)
496
+ const decodeMs = performance.now() - decodeStart
497
+ if (!decoded.ok || !decoded.envelope) {
498
+ socket.send(
499
+ Buffer.from(
500
+ JSON.stringify({ id: message.id, ok: false, queueMs, decodeMs, verifyMs: 0, error: 'decode failed' }),
501
+ 'utf8',
502
+ ),
503
+ )
504
+ return
505
+ }
506
+
507
+ const verifyStart = performance.now()
508
+ const result = await receiveEnvelope(decoded.envelope, {
509
+ nowMs: nowMs(),
510
+ replayCache: this.replayCache,
511
+ verificationMaterialCache: this.verificationMaterialCache,
512
+ secretResolver: SHARED_SECRET_RESOLVER,
513
+ })
514
+ const verifyMs = performance.now() - verifyStart
515
+
516
+ socket.send(
517
+ Buffer.from(
518
+ JSON.stringify({
519
+ id: message.id,
520
+ ok: result.ok,
521
+ queueMs,
522
+ decodeMs,
523
+ verifyMs,
524
+ error: result.ok ? undefined : result.diagnostics.map((d) => d.message).join('; '),
525
+ }),
526
+ 'utf8',
527
+ ),
528
+ )
529
+ } catch {
530
+ socket.send(
531
+ Buffer.from(
532
+ JSON.stringify({ id: message.id, ok: false, queueMs, decodeMs: 0, verifyMs: 0, error: 'internal ws receive error' }),
533
+ 'utf8',
534
+ ),
535
+ )
536
+ }
537
+ })
538
+ })
539
+
540
+ const port = await new Promise<number>((resolve, reject) => {
541
+ this.server?.once('error', reject)
542
+ this.server?.once('listening', () => {
543
+ const address = this.server?.address()
544
+ if (!address || typeof address === 'string') {
545
+ reject(new Error('Could not resolve WS server address'))
546
+ return
547
+ }
548
+ resolve(address.port)
549
+ })
550
+ })
551
+
552
+ this.client = await new Promise<WebSocket>((resolve, reject) => {
553
+ const socket = new WebSocket(`ws://127.0.0.1:${port}`)
554
+ socket.once('open', () => resolve(socket))
555
+ socket.once('error', reject)
556
+ })
557
+
558
+ this.client.on('message', (data) => {
559
+ let frame: { id: number; ok: boolean; queueMs: number; verifyMs: number; decodeMs: number; error?: string }
560
+ try {
561
+ frame = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data)) as {
562
+ id: number
563
+ ok: boolean
564
+ queueMs: number
565
+ verifyMs: number
566
+ decodeMs: number
567
+ error?: string
568
+ }
569
+ } catch {
570
+ return
571
+ }
572
+ const pending = this.pending.get(frame.id)
573
+ if (!pending) return
574
+ this.pending.delete(frame.id)
575
+ if (!frame.ok) {
576
+ pending.reject(new Error(frame.error ?? 'ws adapter received non-ok response'))
577
+ return
578
+ }
579
+ pending.resolve({
580
+ queueMs: frame.queueMs,
581
+ transportMs: 0,
582
+ verifyMs: frame.verifyMs,
583
+ decodeMs: frame.decodeMs,
584
+ })
585
+ })
586
+ }
587
+
588
+ async send(rawEnvelope: string): Promise<TransportTrace> {
589
+ if (!this.client || this.client.readyState !== WebSocket.OPEN) {
590
+ throw new Error('WebSocket client is not open')
591
+ }
592
+
593
+ const id = this.nextId
594
+ this.nextId += 1
595
+ const sendStart = performance.now()
596
+
597
+ const completion = new Promise<TransportTrace>((resolve, reject) => {
598
+ this.pending.set(id, { resolve, reject })
599
+ })
600
+
601
+ this.client.send(Buffer.from(JSON.stringify({ id, payload: rawEnvelope }), 'utf8'))
602
+ const trace = await completion
603
+ return {
604
+ ...trace,
605
+ transportMs: performance.now() - sendStart,
606
+ }
607
+ }
608
+
609
+ async stop(): Promise<void> {
610
+ if (this.client) {
611
+ await new Promise<void>((resolve) => {
612
+ this.client?.once('close', () => resolve())
613
+ this.client?.close()
614
+ })
615
+ this.client = null
616
+ }
617
+
618
+ if (this.server) {
619
+ await new Promise<void>((resolve, reject) => {
620
+ this.server?.close((error) => (error ? reject(error) : resolve()))
621
+ })
622
+ this.server = null
623
+ }
624
+ }
625
+ }
626
+
627
+ async function runAgentLoop(payload: string): Promise<void> {
628
+ const input = payload
629
+ const plan = `plan:${input.length}`
630
+ const toolResult = await Promise.resolve(plan.length + input.length)
631
+ const memoryUpdate = `${plan}:${toolResult}`
632
+ const response = `ok:${memoryUpdate}`
633
+ if (response.length === 0) {
634
+ throw new Error('unexpected empty response')
635
+ }
636
+ }
637
+
638
+ async function buildSignAndEncodeEnvelope(
639
+ template: BenchEnvelopeTemplate,
640
+ payload: string,
641
+ sequence: number,
642
+ ): Promise<{
643
+ raw: string
644
+ buildMs: number
645
+ canonicalizeMs: number
646
+ macMs: number
647
+ }> {
648
+ const buildStart = performance.now()
649
+ const unsigned = buildEnvelope(template, payload, sequence)
650
+ const buildMs = performance.now() - buildStart
651
+
652
+ const canonicalizeStart = performance.now()
653
+ const canonicalPayload = canonicalizeEnvelope(unsigned)
654
+ const canonicalizeMs = performance.now() - canonicalizeStart
655
+
656
+ const macStart = performance.now()
657
+ const signature = await signCanonicalPayloadHmac(canonicalPayload, SHARED_SECRET)
658
+ const macMs = performance.now() - macStart
659
+
660
+ const signedEnvelope: ProtocolEnvelope = {
661
+ ...unsigned,
662
+ signature: {
663
+ alg: 'HS256',
664
+ keyId: 'bench-k1',
665
+ value: signature,
666
+ },
667
+ }
668
+
669
+ return {
670
+ raw: encodeEnvelope(signedEnvelope, 'compact'),
671
+ buildMs,
672
+ canonicalizeMs,
673
+ macMs,
674
+ }
675
+ }
676
+
677
+ async function runPhase(options: {
678
+ durationMs: number
679
+ concurrency: number
680
+ payload: string
681
+ execute: (payload: string) => Promise<TransportTrace & { buildMs: number; canonicalizeMs: number; macMs: number }>
682
+ collectLatencies: boolean
683
+ }): Promise<{ latencies: number[]; completed: number; stages: StageTotals }> {
684
+ const latencies: number[] = []
685
+ const stages = emptyStageTotals()
686
+ const deadline = performance.now() + options.durationMs
687
+ let inFlight = 0
688
+ let completed = 0
689
+
690
+ return new Promise((resolve, reject) => {
691
+ let settled = false
692
+
693
+ const finish = () => {
694
+ if (settled) return
695
+ settled = true
696
+ resolve({ latencies, completed, stages })
697
+ }
698
+
699
+ const maybeLaunch = () => {
700
+ while (inFlight < options.concurrency && performance.now() < deadline) {
701
+ inFlight += 1
702
+ const opStart = performance.now()
703
+
704
+ options
705
+ .execute(options.payload)
706
+ .then((trace) => {
707
+ completed += 1
708
+ addStageTotals(stages, trace)
709
+ if (options.collectLatencies) {
710
+ const elapsed = performance.now() - opStart
711
+ if (latencies.length < MAX_LATENCY_SAMPLES) {
712
+ latencies.push(elapsed)
713
+ } else {
714
+ const sampleIndex = Math.floor(Math.random() * completed)
715
+ if (sampleIndex < MAX_LATENCY_SAMPLES) {
716
+ latencies[sampleIndex] = elapsed
717
+ }
718
+ }
719
+ }
720
+ })
721
+ .catch((error: unknown) => {
722
+ if (settled) return
723
+ settled = true
724
+ reject(error)
725
+ })
726
+ .finally(() => {
727
+ inFlight -= 1
728
+ if (settled) return
729
+ if (performance.now() < deadline) {
730
+ maybeLaunch()
731
+ return
732
+ }
733
+ if (inFlight === 0) {
734
+ finish()
735
+ }
736
+ })
737
+ }
738
+ }
739
+
740
+ maybeLaunch()
741
+ if (inFlight === 0) {
742
+ finish()
743
+ }
744
+ })
745
+ }
746
+
747
+ async function runScenario(profile: Profile, scenario: Scenario): Promise<ScenarioResult> {
748
+ const payload = payloadOfSize(scenario.payloadBytes)
749
+ const template: BenchEnvelopeTemplate = {
750
+ headerBase: {
751
+ version: '7h3/0.1',
752
+ ttlMs: 120_000,
753
+ sender: 'bench.sender',
754
+ recipient: 'bench.receiver',
755
+ },
756
+ intent: 'TASK',
757
+ capability: 'task.plan',
758
+ }
759
+
760
+ const adapter: TransportAdapter | null =
761
+ scenario.mode === 'inproc' ? new InProcessAdapter() : scenario.mode === 'http' ? new HttpAdapter() : scenario.mode === 'ws' ? new WsAdapter() : null
762
+
763
+ try {
764
+ if (adapter) await adapter.start()
765
+
766
+ let sequence = 0
767
+ const execute = async (content: string): Promise<TransportTrace & { buildMs: number; canonicalizeMs: number; macMs: number }> => {
768
+ if (scenario.mode === 'agent-loop') {
769
+ const buildStart = performance.now()
770
+ await runAgentLoop(content)
771
+ return {
772
+ buildMs: performance.now() - buildStart,
773
+ canonicalizeMs: 0,
774
+ macMs: 0,
775
+ queueMs: 0,
776
+ transportMs: 0,
777
+ verifyMs: 0,
778
+ decodeMs: 0,
779
+ }
780
+ }
781
+
782
+ sequence += 1
783
+ const built = await buildSignAndEncodeEnvelope(template, content, sequence)
784
+ const trace = await adapter!.send(built.raw)
785
+ return {
786
+ ...trace,
787
+ buildMs: built.buildMs,
788
+ canonicalizeMs: built.canonicalizeMs,
789
+ macMs: built.macMs,
790
+ }
791
+ }
792
+
793
+ await runPhase({
794
+ durationMs: profile.warmupMs,
795
+ concurrency: scenario.concurrency,
796
+ payload,
797
+ execute,
798
+ collectLatencies: false,
799
+ })
800
+
801
+ maybeGc()
802
+ const cpuStart = process.cpuUsage()
803
+ const memStart = process.memoryUsage()
804
+
805
+ const measured = await runPhase({
806
+ durationMs: profile.measureMs,
807
+ concurrency: scenario.concurrency,
808
+ payload,
809
+ execute,
810
+ collectLatencies: true,
811
+ })
812
+
813
+ const cpuEnd = process.cpuUsage(cpuStart)
814
+ const memEnd = process.memoryUsage()
815
+ const elapsedSeconds = profile.measureMs / 1000
816
+
817
+ const sortedLatencies = measured.latencies.slice().sort((a, b) => a - b)
818
+ const completed = measured.completed
819
+ const opsPerSecond = completed / elapsedSeconds
820
+ const cpuMs = (cpuEnd.user + cpuEnd.system) / 1000
821
+ const stageScale = completed > 0 ? 1 / completed : 0
822
+
823
+ const cpuMsPer10kOps = completed > 0 ? cpuMs / (completed / 10_000) : 0
824
+ const heapDeltaBytes = memEnd.heapUsed - memStart.heapUsed
825
+ const rssDeltaBytes = memEnd.rss - memStart.rss
826
+
827
+ return {
828
+ mode: scenario.mode,
829
+ payloadBytes: scenario.payloadBytes,
830
+ concurrency: scenario.concurrency,
831
+ ops: completed,
832
+ opsPerSecond: round3(opsPerSecond),
833
+ p50Ms: round3(percentile(sortedLatencies, 50)),
834
+ p95Ms: round3(percentile(sortedLatencies, 95)),
835
+ p99Ms: round3(percentile(sortedLatencies, 99)),
836
+ cpuMsPer10kOps: round3(cpuMsPer10kOps),
837
+ heapDeltaMb: round3(heapDeltaBytes / (1024 * 1024)),
838
+ rssDeltaMb: round3(rssDeltaBytes / (1024 * 1024)),
839
+ allocBytesPerOp: round3(completed > 0 ? heapDeltaBytes / completed : 0),
840
+ stageBuildMsPerOp: round3(measured.stages.buildMs * stageScale),
841
+ stageCanonicalizeMsPerOp: round3(measured.stages.canonicalizeMs * stageScale),
842
+ stageMacMsPerOp: round3(measured.stages.macMs * stageScale),
843
+ stageQueueMsPerOp: round3(measured.stages.queueMs * stageScale),
844
+ stageTransportMsPerOp: round3(measured.stages.transportMs * stageScale),
845
+ stageVerifyMsPerOp: round3(measured.stages.verifyMs * stageScale),
846
+ stageDecodeMsPerOp: round3(measured.stages.decodeMs * stageScale),
847
+ }
848
+ } finally {
849
+ if (adapter) await adapter.stop()
850
+ }
851
+ }
852
+
853
+ function renderMarkdown(profileName: ProfileName, profile: Profile, rows: ScenarioResult[]): string {
854
+ const lines: string[] = []
855
+ lines.push(`# Protocol E2E Benchmark (${profileName})`)
856
+ lines.push('')
857
+ lines.push(`- Warmup: ${profile.warmupMs}ms`)
858
+ lines.push(`- Measure: ${profile.measureMs}ms`)
859
+ lines.push('- Pipeline: build -> canonicalize -> MAC -> queue -> transport -> verify -> decode')
860
+ lines.push('')
861
+ lines.push('| mode | payload | concurrency | ops/s | p50 ms | p95 ms | p99 ms | cpu/10k ms | build ms/op | canon ms/op | mac ms/op | queue ms/op | transport ms/op | verify ms/op | decode ms/op |')
862
+ lines.push('| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |')
863
+
864
+ for (const row of rows) {
865
+ lines.push(
866
+ `| ${row.mode} | ${row.payloadBytes} | ${row.concurrency} | ${row.opsPerSecond} | ${row.p50Ms} | ${row.p95Ms} | ${row.p99Ms} | ${row.cpuMsPer10kOps} | ${row.stageBuildMsPerOp} | ${row.stageCanonicalizeMsPerOp} | ${row.stageMacMsPerOp} | ${row.stageQueueMsPerOp} | ${row.stageTransportMsPerOp} | ${row.stageVerifyMsPerOp} | ${row.stageDecodeMsPerOp} |`,
867
+ )
868
+ }
869
+
870
+ return `${lines.join('\n')}\n`
871
+ }
872
+
873
+ async function main(): Promise<void> {
874
+ const profileName = parseProfile(process.argv)
875
+ const profile = PROFILES[profileName]
876
+ if (!profile) {
877
+ throw new Error(`Unknown profile '${profileName}'`)
878
+ }
879
+
880
+ const modes = parseModeSelection(process.argv, ['inproc', 'http', 'ws', 'agent-loop'])
881
+ const payloadSizes = parseNumberSelection(process.argv, '--payloads', PAYLOAD_SIZES)
882
+ const concurrencyLevels = parseNumberSelection(process.argv, '--concurrency', CONCURRENCY_LEVELS)
883
+ const scenarios: Scenario[] = []
884
+ for (const mode of modes) {
885
+ for (const payloadBytes of payloadSizes) {
886
+ for (const concurrency of concurrencyLevels) {
887
+ scenarios.push({ mode, payloadBytes, concurrency })
888
+ }
889
+ }
890
+ }
891
+
892
+ const rows: ScenarioResult[] = []
893
+ for (const scenario of scenarios) {
894
+ const label = `[${scenario.mode}] payload=${scenario.payloadBytes}B concurrency=${scenario.concurrency}`
895
+ console.log(`Running ${label}`)
896
+ const result = await runScenario(profile, scenario)
897
+ rows.push(result)
898
+ console.log(
899
+ ` ops/s=${result.opsPerSecond} p99=${result.p99Ms}ms build=${result.stageBuildMsPerOp}ms canon=${result.stageCanonicalizeMsPerOp}ms mac=${result.stageMacMsPerOp}ms transport=${result.stageTransportMsPerOp}ms verify=${result.stageVerifyMsPerOp}ms`,
900
+ )
901
+ }
902
+
903
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
904
+ const outputDir = 'dist/bench'
905
+ await mkdir(outputDir, { recursive: true })
906
+
907
+ const jsonPath = `${outputDir}/protocol-e2e.${profileName}.${timestamp}.json`
908
+ const mdPath = `${outputDir}/protocol-e2e.${profileName}.${timestamp}.md`
909
+ const latestJsonPath = `${outputDir}/protocol-e2e.${profileName}.latest.json`
910
+ const latestMdPath = `${outputDir}/protocol-e2e.${profileName}.latest.md`
911
+
912
+ const payload = {
913
+ profile: profileName,
914
+ warmupMs: profile.warmupMs,
915
+ measureMs: profile.measureMs,
916
+ results: rows,
917
+ }
918
+
919
+ await writeFile(jsonPath, JSON.stringify(payload, null, 2), 'utf8')
920
+ await writeFile(latestJsonPath, JSON.stringify(payload, null, 2), 'utf8')
921
+
922
+ const markdown = renderMarkdown(profileName, profile, rows)
923
+ await writeFile(mdPath, markdown, 'utf8')
924
+ await writeFile(latestMdPath, markdown, 'utf8')
925
+
926
+ console.log('')
927
+ console.log(`Wrote ${jsonPath}`)
928
+ console.log(`Wrote ${mdPath}`)
929
+ console.log(`Updated ${latestJsonPath}`)
930
+ console.log(`Updated ${latestMdPath}`)
931
+ }
932
+
933
+ main().catch((error: unknown) => {
934
+ const message = error instanceof Error ? error.stack ?? error.message : String(error)
935
+ console.error(message)
936
+ process.exitCode = 1
937
+ })