@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,72 @@
1
+ import type { ProtocolEnvelope, SignatureVerificationMaterial } from './protocol'
2
+
3
+ export type KeyStatus = 'active' | 'verify-only' | 'revoked'
4
+
5
+ export interface KeyRecord {
6
+ sender: string
7
+ keyId: string
8
+ alg: SignatureVerificationMaterial['alg']
9
+ status: KeyStatus
10
+ notBeforeMs: number
11
+ notAfterMs: number
12
+ material: SignatureVerificationMaterial
13
+ }
14
+
15
+ export class RollingKeyring {
16
+ private readonly records: KeyRecord[]
17
+
18
+ constructor(records: KeyRecord[] = []) {
19
+ this.records = [...records]
20
+ }
21
+
22
+ add(record: KeyRecord): void {
23
+ this.records.push(record)
24
+ }
25
+
26
+ revoke(sender: string, keyId: string, revokedAtMs = Date.now()): boolean {
27
+ const record = this.records.find((entry) => entry.sender === sender && entry.keyId === keyId)
28
+ if (!record) return false
29
+ record.status = 'revoked'
30
+ record.notAfterMs = Math.min(record.notAfterMs, revokedAtMs)
31
+ return true
32
+ }
33
+
34
+ selectSigningKey(sender: string, alg: SignatureVerificationMaterial['alg'], nowMs = Date.now()): KeyRecord | null {
35
+ const candidates = this.records
36
+ .filter(
37
+ (entry) =>
38
+ entry.sender === sender &&
39
+ entry.alg === alg &&
40
+ entry.status === 'active' &&
41
+ entry.notBeforeMs <= nowMs &&
42
+ nowMs < entry.notAfterMs,
43
+ )
44
+ .sort((a, b) => b.notBeforeMs - a.notBeforeMs)
45
+ return candidates[0] ?? null
46
+ }
47
+
48
+ resolveVerificationMaterial(
49
+ signature: NonNullable<ProtocolEnvelope['signature']>,
50
+ sender: string,
51
+ nowMs = Date.now(),
52
+ ): SignatureVerificationMaterial | undefined {
53
+ const candidates = this.records
54
+ .filter(
55
+ (entry) =>
56
+ entry.sender === sender &&
57
+ entry.keyId === signature.keyId &&
58
+ entry.alg === signature.alg &&
59
+ entry.status !== 'revoked' &&
60
+ entry.notBeforeMs <= nowMs &&
61
+ nowMs < entry.notAfterMs,
62
+ )
63
+ .sort((a, b) => b.notBeforeMs - a.notBeforeMs)
64
+
65
+ return candidates[0]?.material
66
+ }
67
+ }
68
+
69
+ export function createKeyringSignatureResolver(keyring: RollingKeyring, nowMsProvider: () => number = () => Date.now()) {
70
+ return async (signature: NonNullable<ProtocolEnvelope['signature']>, sender: string): Promise<SignatureVerificationMaterial | undefined> =>
71
+ keyring.resolveVerificationMaterial(signature, sender, nowMsProvider())
72
+ }
@@ -0,0 +1,129 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createAipMcpGatewayRuntime } from './mcpGateway'
3
+
4
+ describe('AIP MCP gateway runtime', () => {
5
+ it('returns JSON-RPC result for allowed method', async () => {
6
+ const runtime = createAipMcpGatewayRuntime({ sharedSecret: 'gateway-test-secret' })
7
+
8
+ const line = JSON.stringify({
9
+ jsonrpc: '2.0',
10
+ id: 1,
11
+ method: 'tools/call',
12
+ params: { name: 'planner' },
13
+ })
14
+
15
+ const responseLine = await runtime.handleLine(line)
16
+ expect(responseLine).not.toBeNull()
17
+ const response = JSON.parse(responseLine ?? '{}') as {
18
+ jsonrpc: string
19
+ id: number
20
+ result?: { ok: boolean; capability: string }
21
+ error?: { code: number; message: string }
22
+ }
23
+
24
+ expect(response.jsonrpc).toBe('2.0')
25
+ expect(response.id).toBe(1)
26
+ expect(response.error).toBeUndefined()
27
+ expect(response.result?.ok).toBe(true)
28
+ expect(response.result?.capability).toBe('mcp.tools/call')
29
+ })
30
+
31
+ it('returns invalid request for malformed JSON-RPC line', async () => {
32
+ const runtime = createAipMcpGatewayRuntime({ sharedSecret: 'gateway-test-secret' })
33
+ const responseLine = await runtime.handleLine('{"bad": true}')
34
+ const response = JSON.parse(responseLine ?? '{}') as { error?: { code: number; message: string } }
35
+
36
+ expect(response.error?.code).toBe(-32600)
37
+ expect(response.error?.message).toBe('Invalid Request')
38
+ })
39
+
40
+ it('returns method not found for disallowed method', async () => {
41
+ const runtime = createAipMcpGatewayRuntime({
42
+ sharedSecret: 'gateway-test-secret',
43
+ allowedMethods: ['tools/call'],
44
+ })
45
+
46
+ const line = JSON.stringify({
47
+ jsonrpc: '2.0',
48
+ id: 'x',
49
+ method: 'resources/read',
50
+ params: {},
51
+ })
52
+
53
+ const responseLine = await runtime.handleLine(line)
54
+ const response = JSON.parse(responseLine ?? '{}') as { error?: { code: number; message: string } }
55
+
56
+ expect(response.error?.code).toBe(-32601)
57
+ expect(response.error?.message).toContain("is not allowed")
58
+ })
59
+
60
+ it('returns authorization error when policy hook blocks request', async () => {
61
+ const runtime = createAipMcpGatewayRuntime({
62
+ sharedSecret: 'gateway-test-secret',
63
+ authorizeMethod: async () => false,
64
+ })
65
+
66
+ const line = JSON.stringify({
67
+ jsonrpc: '2.0',
68
+ id: 'auth-2',
69
+ method: 'tools/call',
70
+ params: {},
71
+ })
72
+
73
+ const responseLine = await runtime.handleLine(line)
74
+ const response = JSON.parse(responseLine ?? '{}') as { error?: { code: number; message: string } }
75
+
76
+ expect(response.error?.code).toBe(-32001)
77
+ expect(response.error?.message).toContain('not authorized')
78
+ })
79
+
80
+ it('returns rate-limit error when limiter blocks request', async () => {
81
+ const runtime = createAipMcpGatewayRuntime({
82
+ sharedSecret: 'gateway-test-secret',
83
+ rateLimiter: {
84
+ consume: async () => false,
85
+ },
86
+ })
87
+
88
+ const line = JSON.stringify({
89
+ jsonrpc: '2.0',
90
+ id: 'rl-3',
91
+ method: 'tools/call',
92
+ params: {},
93
+ })
94
+
95
+ const responseLine = await runtime.handleLine(line)
96
+ const response = JSON.parse(responseLine ?? '{}') as { error?: { code: number; message: string } }
97
+
98
+ expect(response.error?.code).toBe(-32002)
99
+ expect(response.error?.message).toContain('rate-limited')
100
+ })
101
+
102
+ it('ignores empty lines', async () => {
103
+ const runtime = createAipMcpGatewayRuntime({ sharedSecret: 'gateway-test-secret' })
104
+ await expect(runtime.handleLine(' ')).resolves.toBeNull()
105
+ })
106
+
107
+ it('emits audit events for policy and request phases', async () => {
108
+ const phases: string[] = []
109
+ const runtime = createAipMcpGatewayRuntime({
110
+ sharedSecret: 'gateway-test-secret',
111
+ onAuditEvent: async (event) => {
112
+ phases.push(event.phase)
113
+ },
114
+ })
115
+
116
+ const line = JSON.stringify({
117
+ jsonrpc: '2.0',
118
+ id: 'audit-1',
119
+ method: 'tools/call',
120
+ params: { name: 'planner' },
121
+ })
122
+
123
+ const responseLine = await runtime.handleLine(line)
124
+ expect(responseLine).not.toBeNull()
125
+ expect(phases).toContain('request_received')
126
+ expect(phases).toContain('policy')
127
+ expect(phases).toContain('request_success')
128
+ })
129
+ })
@@ -0,0 +1,250 @@
1
+ import { createAipAgentAdapter } from './agentAdapter'
2
+ import { bootstrapRuntimePolicyEnforcer, type BootstrapRuntimePolicyEnforcerOptions } from './policyEnforcer'
3
+ import {
4
+ createRawTaskFromJsonRpc,
5
+ type JsonRpcPolicyEvent,
6
+ toJsonRpcResponse,
7
+ type JsonRpcBridgeOptions,
8
+ type JsonRpcRequestLike,
9
+ } from './frameworkAdapters'
10
+ import { type WireEnvelope } from './protocolTransport'
11
+
12
+ export interface AipMcpGatewayOptions {
13
+ sharedSecret: string
14
+ gatewayAgentId?: string
15
+ workerAgentId?: string
16
+ allowedMethods?: readonly string[]
17
+ workerCapabilityPrefix?: string
18
+ methodToCapability?: JsonRpcBridgeOptions['methodToCapability']
19
+ authorizeMethod?: JsonRpcBridgeOptions['authorizeMethod']
20
+ rateLimitKey?: JsonRpcBridgeOptions['rateLimitKey']
21
+ rateLimiter?: JsonRpcBridgeOptions['rateLimiter']
22
+ onAuditEvent?: (event: AipMcpGatewayAuditEvent) => void | Promise<void>
23
+ gatewayWireFormat?: 'json' | 'compact' | 'binary'
24
+ workerWireFormat?: 'json' | 'compact' | 'binary'
25
+ runtimePolicy?: {
26
+ enabled?: boolean
27
+ options?: BootstrapRuntimePolicyEnforcerOptions
28
+ concurrencyHint?: number
29
+ latencySensitive?: boolean
30
+ compatibilityFirst?: boolean
31
+ }
32
+ }
33
+
34
+ export interface AipMcpGatewayRuntime {
35
+ handleLine: (line: string) => Promise<string | null>
36
+ }
37
+
38
+ export interface AipMcpGatewayAuditEvent {
39
+ phase: 'request_received' | 'policy' | 'verification_failed' | 'request_success' | 'request_error'
40
+ id?: string | number | null
41
+ method?: string
42
+ code?: number
43
+ message?: string
44
+ policy?: JsonRpcPolicyEvent
45
+ timestampMs: number
46
+ }
47
+
48
+ const DEFAULT_ALLOWED_METHODS = ['tools/call', 'resources/read', 'prompts/get']
49
+
50
+ function jsonString(value: unknown): string {
51
+ return JSON.stringify(value)
52
+ }
53
+
54
+ function createErrorResponse(id: string | number | null, code: number, message: string): string {
55
+ return jsonString({
56
+ jsonrpc: '2.0',
57
+ id,
58
+ error: { code, message },
59
+ })
60
+ }
61
+
62
+ function parseRequest(raw: string): JsonRpcRequestLike | null {
63
+ const parsed = JSON.parse(raw) as Partial<JsonRpcRequestLike>
64
+ if (parsed.jsonrpc !== '2.0') return null
65
+ if (typeof parsed.method !== 'string' || parsed.method.length === 0) return null
66
+ if (typeof parsed.id !== 'string' && typeof parsed.id !== 'number') return null
67
+
68
+ return {
69
+ jsonrpc: '2.0',
70
+ id: parsed.id,
71
+ method: parsed.method,
72
+ params: parsed.params,
73
+ }
74
+ }
75
+
76
+ function mapBridgeErrorCode(message: string): number {
77
+ if (message.includes('not allowed')) return -32601
78
+ if (message.includes('not authorized')) return -32001
79
+ if (message.includes('rate-limited')) return -32002
80
+ return -32603
81
+ }
82
+
83
+ export function createAipMcpGatewayRuntime(options: AipMcpGatewayOptions): AipMcpGatewayRuntime {
84
+ const gatewayAgentId = options.gatewayAgentId ?? 'agent.gateway'
85
+ const workerAgentId = options.workerAgentId ?? 'agent.worker'
86
+ const allowedMethods = options.allowedMethods ?? DEFAULT_ALLOWED_METHODS
87
+ const workerCapabilityPrefix = options.workerCapabilityPrefix ?? 'mcp.'
88
+ const gatewayWireFormat = options.gatewayWireFormat ?? 'compact'
89
+ const workerWireFormat = options.workerWireFormat ?? 'compact'
90
+
91
+ async function emitAuditEvent(event: Omit<AipMcpGatewayAuditEvent, 'timestampMs'>): Promise<void> {
92
+ if (options.onAuditEvent) {
93
+ await options.onAuditEvent({
94
+ ...event,
95
+ timestampMs: Date.now(),
96
+ })
97
+ }
98
+ }
99
+
100
+ const gateway = createAipAgentAdapter({
101
+ agentId: gatewayAgentId,
102
+ outboundSecret: options.sharedSecret,
103
+ sharedSecrets: { [workerAgentId]: options.sharedSecret },
104
+ wireFormat: gatewayWireFormat,
105
+ })
106
+
107
+ const worker = createAipAgentAdapter({
108
+ agentId: workerAgentId,
109
+ outboundSecret: options.sharedSecret,
110
+ sharedSecrets: { [gatewayAgentId]: options.sharedSecret },
111
+ wireFormat: workerWireFormat,
112
+ onTask: async (envelope) => ({
113
+ intent: 'RESULT',
114
+ content: jsonString({
115
+ ok: true,
116
+ capability: envelope.body.capability,
117
+ content: envelope.body.content,
118
+ }),
119
+ capability: envelope.body.capability?.startsWith(workerCapabilityPrefix)
120
+ ? envelope.body.capability
121
+ : `${workerCapabilityPrefix}${envelope.body.capability ?? 'unknown'}`,
122
+ }),
123
+ })
124
+
125
+ return {
126
+ async handleLine(line: string): Promise<string | null> {
127
+ const raw = line.trim()
128
+ if (!raw) return null
129
+
130
+ let id: string | number | null = null
131
+ try {
132
+ const request = parseRequest(raw)
133
+ if (!request) {
134
+ await emitAuditEvent({
135
+ phase: 'request_error',
136
+ code: -32600,
137
+ message: 'Invalid Request',
138
+ })
139
+ return createErrorResponse(null, -32600, 'Invalid Request')
140
+ }
141
+ id = request.id
142
+ await emitAuditEvent({
143
+ phase: 'request_received',
144
+ id: request.id,
145
+ method: request.method,
146
+ })
147
+
148
+ const bridgeOptions: JsonRpcBridgeOptions = {
149
+ recipient: workerAgentId,
150
+ allowedMethods,
151
+ methodToCapability: options.methodToCapability,
152
+ authorizeMethod: options.authorizeMethod,
153
+ rateLimitKey: options.rateLimitKey,
154
+ rateLimiter: options.rateLimiter,
155
+ onPolicyEvent: async (event) => {
156
+ await emitAuditEvent({
157
+ phase: 'policy',
158
+ id: request.id,
159
+ method: request.method,
160
+ policy: event,
161
+ })
162
+ },
163
+ }
164
+
165
+ const outbound = await createRawTaskFromJsonRpc(gateway, request, bridgeOptions)
166
+ let rawResponse: WireEnvelope | null = null
167
+ await worker.handleRaw(outbound, async (responseRaw) => {
168
+ rawResponse = responseRaw
169
+ })
170
+ if (!rawResponse) {
171
+ await emitAuditEvent({
172
+ phase: 'verification_failed',
173
+ id: request.id,
174
+ method: request.method,
175
+ code: -32603,
176
+ message: 'AIP worker did not produce a response',
177
+ })
178
+ return createErrorResponse(id, -32603, 'AIP worker did not produce a response')
179
+ }
180
+
181
+ const verified = await gateway.receiveRaw(rawResponse)
182
+ if (!verified.ok || !verified.received) {
183
+ await emitAuditEvent({
184
+ phase: 'verification_failed',
185
+ id: request.id,
186
+ method: request.method,
187
+ code: -32603,
188
+ message: 'AIP verification failed',
189
+ })
190
+ return createErrorResponse(id, -32603, 'AIP verification failed')
191
+ }
192
+
193
+ await emitAuditEvent({
194
+ phase: 'request_success',
195
+ id: request.id,
196
+ method: request.method,
197
+ })
198
+
199
+ return jsonString(toJsonRpcResponse(verified.received, request.id))
200
+ } catch (error: unknown) {
201
+ const message = error instanceof Error ? error.message : 'Internal error'
202
+ const code = mapBridgeErrorCode(message)
203
+ await emitAuditEvent({
204
+ phase: 'request_error',
205
+ id,
206
+ code,
207
+ message,
208
+ })
209
+ return createErrorResponse(id, code, message)
210
+ }
211
+ },
212
+ }
213
+ }
214
+
215
+ export async function createAipMcpGatewayRuntimeWithPolicy(options: AipMcpGatewayOptions): Promise<AipMcpGatewayRuntime> {
216
+ if (!options.runtimePolicy?.enabled) {
217
+ return createAipMcpGatewayRuntime(options)
218
+ }
219
+
220
+ const { enforcer } = await bootstrapRuntimePolicyEnforcer(options.runtimePolicy.options)
221
+ enforcer.assertInvariant({
222
+ signatureVerification: true,
223
+ canonicalization: true,
224
+ replayDefense: true,
225
+ ttlClockSkewEnforcement: true,
226
+ })
227
+
228
+ const mode = enforcer.selectMode({
229
+ concurrency: options.runtimePolicy.concurrencyHint ?? 1,
230
+ latencySensitive: options.runtimePolicy.latencySensitive,
231
+ compatibilityFirst: options.runtimePolicy.compatibilityFirst,
232
+ })
233
+
234
+ const wireFormat = mode.includes('binary') ? 'binary' : 'compact'
235
+ return createAipMcpGatewayRuntime({
236
+ ...options,
237
+ gatewayWireFormat: wireFormat,
238
+ workerWireFormat: wireFormat,
239
+ gatewayAgentId: options.gatewayAgentId,
240
+ workerAgentId: options.workerAgentId,
241
+ onAuditEvent: async (event) => {
242
+ if (options.onAuditEvent) {
243
+ await options.onAuditEvent({
244
+ ...event,
245
+ message: event.message ?? `policy-mode=${mode};wire-format=${wireFormat}`,
246
+ })
247
+ }
248
+ },
249
+ })
250
+ }
@@ -0,0 +1,92 @@
1
+ import http from 'node:http'
2
+ import type { AddressInfo } from 'node:net'
3
+ import { PassThrough } from 'node:stream'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import { signEnvelopeHmac } from './protocol'
6
+ import {
7
+ createMcpClientCodec,
8
+ wrapMcpClient,
9
+ wrapMcpServer,
10
+ type JsonRpcRequest,
11
+ type McpSecurityOptions,
12
+ } from './mcpWrapper'
13
+ import { createHttpMcpClient, createHttpMcpHandler, createStdioMcpClient, serveMcpOverStdio } from './mcpTransports'
14
+
15
+ const SECRET = 'mcp-shared-secret'
16
+
17
+ const serverOptions: McpSecurityOptions = {
18
+ selfAgentId: 'agent.server',
19
+ sign: (envelope) => signEnvelopeHmac(envelope, SECRET, 'server-k1'),
20
+ receive: { secretResolver: async () => SECRET },
21
+ wireFormat: 'json',
22
+ }
23
+
24
+ const clientOptions: McpSecurityOptions = {
25
+ selfAgentId: 'agent.client',
26
+ peerAgentId: 'agent.server',
27
+ sign: (envelope) => signEnvelopeHmac(envelope, SECRET, 'client-k1'),
28
+ receive: { secretResolver: async () => SECRET },
29
+ wireFormat: 'json',
30
+ }
31
+
32
+ const echo = async (req: JsonRpcRequest) => ({ jsonrpc: '2.0' as const, id: req.id, result: { echo: req.params } })
33
+
34
+ describe('stdio transport adapter', () => {
35
+ const cleanups: Array<() => void> = []
36
+ afterEach(() => {
37
+ for (const c of cleanups.splice(0)) c()
38
+ })
39
+
40
+ it('round-trips signed MCP messages over newline-delimited streams', async () => {
41
+ const clientToServer = new PassThrough()
42
+ const serverToClient = new PassThrough()
43
+
44
+ const server = serveMcpOverStdio(wrapMcpServer(echo, serverOptions), { input: clientToServer, output: serverToClient })
45
+ const client = createStdioMcpClient({ input: serverToClient, output: clientToServer })
46
+ cleanups.push(() => server.close(), () => client.close())
47
+
48
+ const call = wrapMcpClient(client.send, clientOptions)
49
+
50
+ const first = await call({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'planner' } })
51
+ const second = await call({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'deploy' } })
52
+
53
+ expect(first).toEqual({ jsonrpc: '2.0', id: 1, result: { echo: { name: 'planner' } } })
54
+ expect(second).toEqual({ jsonrpc: '2.0', id: 2, result: { echo: { name: 'deploy' } } })
55
+ })
56
+ })
57
+
58
+ describe('http transport adapter', () => {
59
+ let server: http.Server | undefined
60
+ afterEach(async () => {
61
+ if (server) await new Promise<void>((resolve) => server!.close(() => resolve()))
62
+ server = undefined
63
+ })
64
+
65
+ it('round-trips signed MCP messages over a real HTTP server', async () => {
66
+ server = http.createServer(createHttpMcpHandler(wrapMcpServer(echo, serverOptions)))
67
+ await new Promise<void>((resolve) => server!.listen(0, '127.0.0.1', resolve))
68
+ const { port } = server.address() as AddressInfo
69
+
70
+ const client = createHttpMcpClient({ url: `http://127.0.0.1:${port}` })
71
+ const call = wrapMcpClient(client.send, clientOptions)
72
+
73
+ const response = await call({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'planner' } })
74
+
75
+ expect(response).toEqual({ jsonrpc: '2.0', id: 1, result: { echo: { name: 'planner' } } })
76
+ })
77
+
78
+ it('rejects an unsigned/invalid request over HTTP and returns a signed JSON-RPC error', async () => {
79
+ server = http.createServer(createHttpMcpHandler(wrapMcpServer(echo, serverOptions)))
80
+ await new Promise<void>((resolve) => server!.listen(0, '127.0.0.1', resolve))
81
+ const { port } = server.address() as AddressInfo
82
+
83
+ const client = createHttpMcpClient({ url: `http://127.0.0.1:${port}` })
84
+ const codec = createMcpClientCodec(clientOptions)
85
+
86
+ // a body that is not a valid signed envelope
87
+ const responseRaw = await client.send('{"not":"an envelope"}')
88
+ const response = await codec.decodeResponse(responseRaw)
89
+
90
+ expect(response.error).toBeDefined()
91
+ })
92
+ })