@7h3/protocol 0.5.0 → 0.5.1
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.
- package/README.md +6 -1317
- package/agentAdapter.d.ts +26 -0
- package/auditLog.d.ts +54 -0
- package/capability.d.ts +67 -0
- package/cborCodec.d.ts +34 -0
- package/conformanceVectors.d.ts +20 -0
- package/encryption.d.ts +85 -0
- package/envelopeCbor.d.ts +33 -0
- package/frameworkAdapters.d.ts +72 -0
- package/gateway.d.ts +53 -0
- package/grpcBinding.d.ts +29 -0
- package/httpBinding.d.ts +50 -0
- package/index.d.ts +39 -0
- package/index.js +3662 -0
- package/keyInfra.d.ts +64 -0
- package/keyRegistry.d.ts +9 -0
- package/keyRotation.d.ts +20 -0
- package/mcpGateway.d.ts +37 -0
- package/mcpTransports.d.ts +62 -0
- package/mcpWrapper.d.ts +83 -0
- package/otel.d.ts +65 -0
- package/package.json +13 -121
- package/policyEnforcer.d.ts +50 -0
- package/policyTelemetryFeedback.d.ts +11 -0
- package/{src/protocol.d.ts → protocol.d.ts} +1 -2
- package/protocolAgent.d.ts +58 -0
- package/protocolBinary.d.ts +8 -0
- package/protocolCapabilities.d.ts +24 -0
- package/protocolReplay.d.ts +35 -0
- package/protocolTransport.d.ts +73 -0
- package/queueBinding.d.ts +43 -0
- package/rateLimiter.d.ts +18 -0
- package/{src/redisClient.ts → redisClient.d.ts} +25 -75
- package/replayStores.d.ts +98 -0
- package/revocation.d.ts +71 -0
- package/routePolicy.d.ts +30 -0
- package/runtimePolicy.d.ts +24 -0
- package/runtimePolicyManager.d.ts +15 -0
- package/runtimePolicyPresets.d.ts +11 -0
- package/signedResponse.d.ts +21 -0
- package/stream.d.ts +59 -0
- package/telemetry.d.ts +79 -0
- package/webhookBinding.d.ts +30 -0
- package/wsBinding.d.ts +52 -0
- package/.dockerignore +0 -19
- package/.github/dependabot.yml +0 -32
- package/.github/workflows/ci.yml +0 -31
- package/.github/workflows/publish.yml +0 -59
- package/.github/workflows/scorecard.yml +0 -37
- package/7h3.example.yaml +0 -125
- package/CHANGELOG.md +0 -152
- package/CONTRIBUTING.md +0 -82
- package/Dockerfile +0 -73
- package/GOVERNANCE.md +0 -62
- package/SECURITY.md +0 -70
- package/bench-results/replay-cache-full-1777891033256.json +0 -10
- package/bench-results/replay-cache-full-1777896317488.json +0 -10
- package/bench-results/replay-cache-full-1777900993184.json +0 -10
- package/bench-results/replay-cache-full-1777901019285.json +0 -10
- package/bench-results/replay-cache-quick-1777870170126.json +0 -10
- package/bench-results/signature-profiles-quick-1775875160079.json +0 -85
- package/bench-results/signature-profiles-quick-1775983539716.json +0 -85
- package/bench-results/signature-profiles-quick-1776237913190.json +0 -85
- package/bench-results/wire-codecs-full-1777891019803.json +0 -93
- package/bench-results/wire-codecs-full-1777896260964.json +0 -93
- package/bench-results/wire-codecs-full-1777901004247.json +0 -93
- package/bench-results/wire-codecs-quick-1775972879056.json +0 -93
- package/bench-results/wire-codecs-quick-1775983541111.json +0 -93
- package/bench-results/wire-codecs-quick-1776237914299.json +0 -93
- package/bench-results/wire-codecs-quick-1777841285236.json +0 -93
- package/bench-results/wire-codecs-quick-1777841321772.json +0 -93
- package/bench-results/wire-codecs-quick-1777841330408.json +0 -93
- package/bench-results/wire-codecs-quick-1777852886082.json +0 -93
- package/bench-results/wire-codecs-quick-1777852988773.json +0 -93
- package/bench-results/wire-codecs-quick-1777870188095.json +0 -93
- package/bench-results/wire-codecs-quick-1777870263918.json +0 -93
- package/bench-results/wire-codecs-quick-1777870455034.json +0 -93
- package/bench-results/wire-codecs-quick-1778816163081.json +0 -93
- package/bench-results/wire-codecs-quick-1778843936130.json +0 -93
- package/bin/7h3.ts +0 -406
- package/conformance/7h3_v0_1.json +0 -77
- package/conformance/7h3_v0_1_binary.json +0 -20
- package/conformance/aip_v0_1_binary.json +0 -20
- package/docker-compose.yaml +0 -77
- package/docs/ADOPTION_PLAN.md +0 -120
- package/docs/AGENTS.md +0 -77
- package/docs/AIP_RFC_v0.1.md +0 -97
- package/docs/AI_DECISION_CARD.md +0 -122
- package/docs/AI_RUNTIME_POLICY.json +0 -126
- package/docs/AI_RUNTIME_POLICY.yaml +0 -110
- package/docs/BACKPRESSURE_TUNING.md +0 -65
- package/docs/BENCHMARK_CLAIM_MATRIX.md +0 -42
- package/docs/BENCHMARK_REPORT_TEMPLATE.md +0 -169
- package/docs/BINARY_CODEC_BENCH.md +0 -23
- package/docs/CLEAN_CLONE_RUNBOOK.md +0 -36
- package/docs/CLOCK_SKEW_POLICY.md +0 -30
- package/docs/DISTRIBUTED_REPLAY.md +0 -142
- package/docs/FUZZ_CAMPAIGN.md +0 -121
- package/docs/GATEWAY.md +0 -195
- package/docs/KEY_MANAGEMENT_POLICY.md +0 -53
- package/docs/KEY_REVOCATION.md +0 -69
- package/docs/MCP_WRAPPER.md +0 -159
- package/docs/MIGRATION_GUIDE.md +0 -40
- package/docs/OPERATORS.md +0 -184
- package/docs/PERF_REGRESSION_POLICY.md +0 -34
- package/docs/PROJECT_EXAMINATION_2026-05-31.md +0 -219
- package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +0 -135
- package/docs/RELEASE_GATE.md +0 -25
- package/docs/RELEASE_NOTES_v0.1.0.md +0 -54
- package/docs/SECURITY_REVIEW_2026-06-05.md +0 -165
- package/docs/TELEMETRY.md +0 -41
- package/docs/THREAT_MODEL.md +0 -89
- package/docs/VERSIONING_POLICY.md +0 -30
- package/docs/assets/banner-github.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/banner.svg +0 -123
- package/eslint.config.js +0 -15
- package/fuzz/ts/harness-decode.ts +0 -136
- package/fuzz/ts/harness-verify.ts +0 -121
- package/fuzz/ts/run.ts +0 -35
- package/mcp-server/README.md +0 -38
- package/mcp-server/package-lock.json +0 -1187
- package/mcp-server/package.json +0 -35
- package/mcp-server/src/index.ts +0 -236
- package/mcp-server/tsconfig.json +0 -14
- package/scripts/aip-framework-quickstart.ts +0 -110
- package/scripts/aip-mcp-gateway.ts +0 -38
- package/scripts/aip-mcp-wrap-demo.ts +0 -72
- package/scripts/aip-quickstart.ts +0 -60
- package/scripts/bench-diff.ts +0 -118
- package/scripts/bench-protocol-e2e.ts +0 -937
- package/scripts/bench-protocol-openloop.ts +0 -1397
- package/scripts/bench-replay-cache.ts +0 -76
- package/scripts/bench-signature-profiles.ts +0 -180
- package/scripts/bench-wire-codecs.ts +0 -161
- package/scripts/build-binary-conformance.ts +0 -36
- package/scripts/build-release-dashboard.ts +0 -175
- package/scripts/canary-rollout.ts +0 -38
- package/scripts/mcpGatewayCli.test.ts +0 -116
- package/scripts/prepare-aip-package.ts +0 -88
- package/scripts/regen-conformance-sigs.ts +0 -18
- package/scripts/release-gate.ts +0 -19
- package/scripts/validate-runtime-policy.ts +0 -18
- package/sdk/browser/index.test.ts +0 -162
- package/sdk/browser/index.ts +0 -257
- package/sdk/browser/package.json +0 -13
- package/sdk/go/cbor.go +0 -551
- package/sdk/go/cbor_test.go +0 -232
- package/sdk/go/encryption.go +0 -280
- package/sdk/go/encryption_test.go +0 -318
- package/sdk/go/go.mod +0 -7
- package/sdk/go/go.sum +0 -4
- package/sdk/go/http.go +0 -135
- package/sdk/go/protocol.go +0 -324
- package/sdk/go/protocol_test.go +0 -334
- package/sdk/go/replay.go +0 -121
- package/sdk/go/replay_test.go +0 -149
- package/sdk/go/webhook.go +0 -136
- package/sdk/pq/package-lock.json +0 -1358
- package/sdk/pq/package.json +0 -42
- package/sdk/pq/src/index.test.ts +0 -143
- package/sdk/pq/src/index.ts +0 -166
- package/sdk/pq/tsconfig.json +0 -14
- package/sdk/pq/vitest.config.ts +0 -7
- package/sdk/python/README.md +0 -18
- package/sdk/python/protocol_7h3/__init__.py +0 -46
- package/sdk/python/protocol_7h3/encryption.py +0 -252
- package/sdk/python/protocol_7h3/http.py +0 -212
- package/sdk/python/protocol_7h3/keys.py +0 -149
- package/sdk/python/protocol_7h3/pq.py +0 -244
- package/sdk/python/protocol_7h3/protocol.py +0 -525
- package/sdk/python/protocol_7h3/queue.py +0 -118
- package/sdk/python/protocol_7h3/replay.py +0 -98
- package/sdk/python/protocol_7h3/webhook.py +0 -116
- package/sdk/python/pyproject.toml +0 -40
- package/sdk/python/tests/test_conformance.py +0 -110
- package/sdk/python/tests/test_encryption.py +0 -206
- package/sdk/python/tests/test_http.py +0 -305
- package/sdk/python/tests/test_keys.py +0 -417
- package/sdk/python/tests/test_queue.py +0 -120
- package/sdk/python/tests/test_webhook.py +0 -345
- package/sdk/rust/Cargo.lock +0 -371
- package/sdk/rust/Cargo.toml +0 -25
- package/sdk/rust/README.md +0 -31
- package/sdk/rust/fuzz/Cargo.toml +0 -29
- package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +0 -46
- package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +0 -11
- package/sdk/rust/src/bin/aip_mcp_gateway.rs +0 -59
- package/sdk/rust/src/http.rs +0 -145
- package/sdk/rust/src/keys.rs +0 -161
- package/sdk/rust/src/lib.rs +0 -688
- package/sdk/rust/src/queue.rs +0 -79
- package/sdk/rust/src/webhook.rs +0 -86
- package/sdk/rust/tests/conformance.rs +0 -148
- package/sdk/rust/tests/gateway.rs +0 -130
- package/sdk/rust/tests/http_webhook_queue.rs +0 -201
- package/sdk/rust/tests/keys.rs +0 -189
- package/sdk/threshold/index.d.ts +0 -68
- package/sdk/threshold/index.d.ts.map +0 -1
- package/sdk/threshold/index.js +0 -254
- package/sdk/threshold/package-lock.json +0 -1361
- package/sdk/threshold/package.json +0 -39
- package/sdk/threshold/src/index.d.ts +0 -68
- package/sdk/threshold/src/index.d.ts.map +0 -1
- package/sdk/threshold/src/index.js +0 -254
- package/sdk/threshold/src/index.test.ts +0 -238
- package/sdk/threshold/src/index.ts +0 -355
- package/sdk/threshold/tsconfig.json +0 -19
- package/sdk/threshold/vitest.config.ts +0 -12
- package/src/agentAdapter.test.ts +0 -48
- package/src/agentAdapter.ts +0 -56
- package/src/auditLog.test.ts +0 -145
- package/src/auditLog.ts +0 -147
- package/src/capability.test.ts +0 -504
- package/src/capability.ts +0 -380
- package/src/cborCodec.test.ts +0 -263
- package/src/cborCodec.ts +0 -339
- package/src/conformance.test.ts +0 -136
- package/src/conformanceVectors.ts +0 -99
- package/src/encryption.test.ts +0 -206
- package/src/encryption.ts +0 -245
- package/src/envelopeCbor.ts +0 -140
- package/src/frameworkAdapters.test.ts +0 -290
- package/src/frameworkAdapters.ts +0 -261
- package/src/gateway.test.ts +0 -343
- package/src/gateway.ts +0 -246
- package/src/grpcBinding.test.ts +0 -211
- package/src/grpcBinding.ts +0 -103
- package/src/httpBinding.test.ts +0 -376
- package/src/httpBinding.ts +0 -189
- package/src/index.ts +0 -39
- package/src/keyInfra.test.ts +0 -278
- package/src/keyInfra.ts +0 -228
- package/src/keyRegistry.ts +0 -59
- package/src/keyRotation.test.ts +0 -78
- package/src/keyRotation.ts +0 -72
- package/src/mcpGateway.test.ts +0 -129
- package/src/mcpGateway.ts +0 -250
- package/src/mcpTransports.test.ts +0 -92
- package/src/mcpTransports.ts +0 -169
- package/src/mcpWrapper.test.ts +0 -179
- package/src/mcpWrapper.ts +0 -206
- package/src/otel.ts +0 -136
- package/src/policyEnforcer.test.ts +0 -99
- package/src/policyEnforcer.ts +0 -169
- package/src/policyTelemetryFeedback.test.ts +0 -25
- package/src/policyTelemetryFeedback.ts +0 -38
- package/src/protocol.bench.ts +0 -37
- package/src/protocol.d.ts.map +0 -1
- package/src/protocol.js +0 -294
- package/src/protocol.test.ts +0 -155
- package/src/protocol.ts +0 -414
- package/src/protocolAgent.test.ts +0 -105
- package/src/protocolAgent.ts +0 -169
- package/src/protocolBinary.test.ts +0 -165
- package/src/protocolBinary.ts +0 -312
- package/src/protocolCapabilities.ts +0 -70
- package/src/protocolFuzz.advanced.test.ts +0 -235
- package/src/protocolFuzz.test.ts +0 -111
- package/src/protocolNegative.test.ts +0 -97
- package/src/protocolReplay.test.ts +0 -71
- package/src/protocolReplay.ts +0 -194
- package/src/protocolTransport.test.ts +0 -556
- package/src/protocolTransport.ts +0 -483
- package/src/queueBinding.test.ts +0 -130
- package/src/queueBinding.ts +0 -102
- package/src/rateLimiter.test.ts +0 -96
- package/src/rateLimiter.ts +0 -46
- package/src/redisIntegration.test.ts +0 -134
- package/src/replayStores.test.ts +0 -273
- package/src/replayStores.ts +0 -215
- package/src/revocation.test.ts +0 -98
- package/src/revocation.ts +0 -0
- package/src/routePolicy.test.ts +0 -87
- package/src/routePolicy.ts +0 -72
- package/src/runtimePolicy.test.ts +0 -49
- package/src/runtimePolicy.ts +0 -81
- package/src/runtimePolicyManager.test.ts +0 -29
- package/src/runtimePolicyManager.ts +0 -50
- package/src/runtimePolicyPresets.ts +0 -43
- package/src/signedResponse.test.ts +0 -111
- package/src/signedResponse.ts +0 -83
- package/src/stream.test.ts +0 -254
- package/src/stream.ts +0 -417
- package/src/telemetry.test.ts +0 -251
- package/src/telemetry.ts +0 -299
- package/src/webhookBinding.test.ts +0 -144
- package/src/webhookBinding.ts +0 -115
- package/src/wsBinding.test.ts +0 -221
- package/src/wsBinding.ts +0 -200
- package/tsconfig.json +0 -15
- package/tsconfig.lib.json +0 -23
- package/vite.lib.config.ts +0 -16
- package/vitest.config.ts +0 -11
package/src/mcpGateway.ts
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
})
|
package/src/mcpTransports.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import readline from 'node:readline'
|
|
2
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
3
|
-
import type { Readable, Writable } from 'node:stream'
|
|
4
|
-
import type { WireEnvelope } from './protocolTransport'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Transport adapters for the MCP hardening wrapper.
|
|
8
|
-
*
|
|
9
|
-
* The wrapper (`wrapMcpServer` / `wrapMcpClient`) is transport-agnostic: it
|
|
10
|
-
* operates on `WireEnvelope`s. These adapters connect it to real transports —
|
|
11
|
-
* newline-delimited stdio and HTTP — so a secured server is deployable as-is.
|
|
12
|
-
*
|
|
13
|
-
* Note: the stdio adapter frames one envelope per line and therefore supports
|
|
14
|
-
* the string wire formats (`json`, `compact`). For `binary`, use HTTP.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
type WireHandler = (raw: WireEnvelope) => Promise<WireEnvelope>
|
|
18
|
-
|
|
19
|
-
function assertStringWire(raw: WireEnvelope): string {
|
|
20
|
-
if (typeof raw !== 'string') {
|
|
21
|
-
throw new Error('stdio adapter supports json/compact (string) wire formats; use the HTTP adapter for binary')
|
|
22
|
-
}
|
|
23
|
-
return raw
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface StdioServerHandle {
|
|
27
|
-
close(): void
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Serves a wrapped MCP handler over newline-delimited streams (default
|
|
32
|
-
* `process.stdin` / `process.stdout`). Lines are processed sequentially so
|
|
33
|
-
* responses are emitted in request order.
|
|
34
|
-
*/
|
|
35
|
-
export function serveMcpOverStdio(
|
|
36
|
-
handle: WireHandler,
|
|
37
|
-
options: { input?: Readable; output?: Writable; onError?: (error: unknown) => void } = {},
|
|
38
|
-
): StdioServerHandle {
|
|
39
|
-
const input = options.input ?? process.stdin
|
|
40
|
-
const output = options.output ?? process.stdout
|
|
41
|
-
const rl = readline.createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY })
|
|
42
|
-
|
|
43
|
-
let chain: Promise<void> = Promise.resolve()
|
|
44
|
-
const onLine = (line: string): void => {
|
|
45
|
-
const raw = line.trim()
|
|
46
|
-
if (!raw) return
|
|
47
|
-
chain = chain.then(async () => {
|
|
48
|
-
try {
|
|
49
|
-
const response = await handle(raw)
|
|
50
|
-
output.write(`${assertStringWire(response)}\n`)
|
|
51
|
-
} catch (error) {
|
|
52
|
-
options.onError?.(error)
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
rl.on('line', onLine)
|
|
58
|
-
return {
|
|
59
|
-
close(): void {
|
|
60
|
-
rl.off('line', onLine)
|
|
61
|
-
rl.close()
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface StdioClientHandle {
|
|
67
|
-
send: WireHandler
|
|
68
|
-
close(): void
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Creates a stdio client `send` function for `wrapMcpClient`. Requests and
|
|
73
|
-
* responses are correlated by order over the single pipe (one envelope per line).
|
|
74
|
-
*/
|
|
75
|
-
export function createStdioMcpClient(options: { input: Readable; output: Writable }): StdioClientHandle {
|
|
76
|
-
const rl = readline.createInterface({ input: options.input, crlfDelay: Number.POSITIVE_INFINITY })
|
|
77
|
-
const pending: Array<(line: string) => void> = []
|
|
78
|
-
|
|
79
|
-
const onLine = (line: string): void => {
|
|
80
|
-
const raw = line.trim()
|
|
81
|
-
if (!raw) return
|
|
82
|
-
const resolve = pending.shift()
|
|
83
|
-
if (resolve) resolve(raw)
|
|
84
|
-
}
|
|
85
|
-
rl.on('line', onLine)
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
send(raw: WireEnvelope): Promise<WireEnvelope> {
|
|
89
|
-
const line = assertStringWire(raw)
|
|
90
|
-
return new Promise<WireEnvelope>((resolve) => {
|
|
91
|
-
pending.push(resolve)
|
|
92
|
-
options.output.write(`${line}\n`)
|
|
93
|
-
})
|
|
94
|
-
},
|
|
95
|
-
close(): void {
|
|
96
|
-
rl.off('line', onLine)
|
|
97
|
-
rl.close()
|
|
98
|
-
},
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Creates a `node:http`-compatible request handler that runs the wrapped MCP
|
|
104
|
-
* handler. The request body is the wire envelope; the response body is the
|
|
105
|
-
* signed reply envelope.
|
|
106
|
-
*/
|
|
107
|
-
export function createHttpMcpHandler(
|
|
108
|
-
handle: WireHandler,
|
|
109
|
-
options: { binary?: boolean } = {},
|
|
110
|
-
): (req: IncomingMessage, res: ServerResponse) => void {
|
|
111
|
-
return (req, res) => {
|
|
112
|
-
const chunks: Buffer[] = []
|
|
113
|
-
req.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
114
|
-
req.on('error', () => {
|
|
115
|
-
res.writeHead(400)
|
|
116
|
-
res.end()
|
|
117
|
-
})
|
|
118
|
-
req.on('end', () => {
|
|
119
|
-
void (async () => {
|
|
120
|
-
try {
|
|
121
|
-
const buffer = Buffer.concat(chunks)
|
|
122
|
-
const raw: WireEnvelope = options.binary ? new Uint8Array(buffer) : buffer.toString('utf8')
|
|
123
|
-
const response = await handle(raw)
|
|
124
|
-
if (typeof response === 'string') {
|
|
125
|
-
res.writeHead(200, { 'content-type': 'application/json' })
|
|
126
|
-
res.end(response)
|
|
127
|
-
} else {
|
|
128
|
-
res.writeHead(200, { 'content-type': 'application/octet-stream' })
|
|
129
|
-
res.end(Buffer.from(response))
|
|
130
|
-
}
|
|
131
|
-
} catch (error) {
|
|
132
|
-
res.writeHead(500, { 'content-type': 'application/json' })
|
|
133
|
-
res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32603, message: error instanceof Error ? error.message : 'internal error' } }))
|
|
134
|
-
}
|
|
135
|
-
})()
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export interface HttpClientHandle {
|
|
141
|
-
send: WireHandler
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Creates an HTTP client `send` function for `wrapMcpClient`. POSTs the request
|
|
146
|
-
* envelope to `url` and returns the response envelope. Uses the global `fetch`
|
|
147
|
-
* by default; inject `fetchImpl` for tests or custom agents.
|
|
148
|
-
*/
|
|
149
|
-
export function createHttpMcpClient(options: {
|
|
150
|
-
url: string
|
|
151
|
-
fetchImpl?: typeof fetch
|
|
152
|
-
binary?: boolean
|
|
153
|
-
headers?: Record<string, string>
|
|
154
|
-
}): HttpClientHandle {
|
|
155
|
-
const doFetch = options.fetchImpl ?? fetch
|
|
156
|
-
const contentType = options.binary ? 'application/octet-stream' : 'application/json'
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
async send(raw: WireEnvelope): Promise<WireEnvelope> {
|
|
160
|
-
const response = await doFetch(options.url, {
|
|
161
|
-
method: 'POST',
|
|
162
|
-
headers: { 'content-type': contentType, ...options.headers },
|
|
163
|
-
body: typeof raw === 'string' ? raw : Buffer.from(raw),
|
|
164
|
-
})
|
|
165
|
-
if (options.binary) return new Uint8Array(await response.arrayBuffer())
|
|
166
|
-
return response.text()
|
|
167
|
-
},
|
|
168
|
-
}
|
|
169
|
-
}
|
package/src/mcpWrapper.test.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { createEnvelope, signEnvelopeHmac } from './protocol'
|
|
3
|
-
import { InMemoryReplayCache } from './protocolReplay'
|
|
4
|
-
import { encodeEnvelope, type WireEnvelope } from './protocolTransport'
|
|
5
|
-
import {
|
|
6
|
-
createMcpClientCodec,
|
|
7
|
-
wrapMcpClient,
|
|
8
|
-
wrapMcpServer,
|
|
9
|
-
type JsonRpcRequest,
|
|
10
|
-
type McpSecurityOptions,
|
|
11
|
-
} from './mcpWrapper'
|
|
12
|
-
|
|
13
|
-
const SECRET = 'mcp-shared-secret'
|
|
14
|
-
|
|
15
|
-
function serverOptions(extra: Partial<McpSecurityOptions> = {}): McpSecurityOptions {
|
|
16
|
-
return {
|
|
17
|
-
selfAgentId: 'agent.server',
|
|
18
|
-
sign: (envelope) => signEnvelopeHmac(envelope, SECRET, 'server-k1'),
|
|
19
|
-
receive: { secretResolver: async () => SECRET },
|
|
20
|
-
wireFormat: 'json',
|
|
21
|
-
...extra,
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function clientOptions(extra: Partial<McpSecurityOptions> = {}): McpSecurityOptions {
|
|
26
|
-
return {
|
|
27
|
-
selfAgentId: 'agent.client',
|
|
28
|
-
peerAgentId: 'agent.server',
|
|
29
|
-
sign: (envelope) => signEnvelopeHmac(envelope, SECRET, 'client-k1'),
|
|
30
|
-
receive: { secretResolver: async () => SECRET },
|
|
31
|
-
wireFormat: 'json',
|
|
32
|
-
...extra,
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const echo: (req: JsonRpcRequest) => Promise<{ jsonrpc: '2.0'; id: JsonRpcRequest['id']; result: unknown }> = async (req) => ({
|
|
37
|
-
jsonrpc: '2.0',
|
|
38
|
-
id: req.id,
|
|
39
|
-
result: { echo: req.params },
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('wrapMcpServer + wrapMcpClient round-trip', () => {
|
|
43
|
-
it('delivers a signed request, runs the handler, and returns a verified result', async () => {
|
|
44
|
-
const server = wrapMcpServer(echo, serverOptions())
|
|
45
|
-
const call = wrapMcpClient((raw) => server(raw), clientOptions())
|
|
46
|
-
|
|
47
|
-
const response = await call({ jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'planner' } })
|
|
48
|
-
|
|
49
|
-
expect(response).toEqual({ jsonrpc: '2.0', id: 7, result: { echo: { name: 'planner' } } })
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('hands the underlying handler PLAIN json-rpc (no envelope, no signature)', async () => {
|
|
53
|
-
const handler = vi.fn(echo)
|
|
54
|
-
const server = wrapMcpServer(handler, serverOptions())
|
|
55
|
-
const call = wrapMcpClient((raw) => server(raw), clientOptions())
|
|
56
|
-
|
|
57
|
-
await call({ jsonrpc: '2.0', id: 1, method: 'resources/read', params: { uri: 'file://x' } })
|
|
58
|
-
|
|
59
|
-
expect(handler).toHaveBeenCalledOnce()
|
|
60
|
-
expect(handler).toHaveBeenCalledWith({ jsonrpc: '2.0', id: 1, method: 'resources/read', params: { uri: 'file://x' } })
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
describe('tamper detection', () => {
|
|
65
|
-
function tamperContent(raw: WireEnvelope): WireEnvelope {
|
|
66
|
-
const env = JSON.parse(raw as string)
|
|
67
|
-
env.body.content = `${env.body.content} TAMPERED`
|
|
68
|
-
return JSON.stringify(env)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
it('a request tampered after signing is rejected by the server (handler never runs)', async () => {
|
|
72
|
-
const handler = vi.fn(echo)
|
|
73
|
-
const server = wrapMcpServer(handler, serverOptions())
|
|
74
|
-
const codec = createMcpClientCodec(clientOptions())
|
|
75
|
-
|
|
76
|
-
const { raw } = await codec.encodeRequest({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'x' } })
|
|
77
|
-
const response = await codec.decodeResponse(await server(tamperContent(raw)))
|
|
78
|
-
|
|
79
|
-
expect(handler).not.toHaveBeenCalled()
|
|
80
|
-
expect(response.error).toBeDefined()
|
|
81
|
-
expect(response.result).toBeUndefined()
|
|
82
|
-
})
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
describe('replay detection', () => {
|
|
86
|
-
it('rejects the second delivery of an identical signed request', async () => {
|
|
87
|
-
const replayCache = new InMemoryReplayCache()
|
|
88
|
-
const handler = vi.fn(echo)
|
|
89
|
-
const server = wrapMcpServer(handler, serverOptions({ receive: { secretResolver: async () => SECRET, replayCache } }))
|
|
90
|
-
const codec = createMcpClientCodec(clientOptions())
|
|
91
|
-
|
|
92
|
-
const { raw } = await codec.encodeRequest({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: {} })
|
|
93
|
-
const first = await codec.decodeResponse(await server(raw))
|
|
94
|
-
const second = await codec.decodeResponse(await server(raw))
|
|
95
|
-
|
|
96
|
-
expect(first.result).toBeDefined()
|
|
97
|
-
expect(second.error).toBeDefined()
|
|
98
|
-
expect(handler).toHaveBeenCalledOnce()
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('defaults to replay protection even when no cache is configured', async () => {
|
|
102
|
-
const handler = vi.fn(echo)
|
|
103
|
-
const server = wrapMcpServer(handler, serverOptions()) // no replayCache provided
|
|
104
|
-
const codec = createMcpClientCodec(clientOptions())
|
|
105
|
-
|
|
106
|
-
const { raw } = await codec.encodeRequest({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: {} })
|
|
107
|
-
await codec.decodeResponse(await server(raw))
|
|
108
|
-
const replayed = await codec.decodeResponse(await server(raw))
|
|
109
|
-
|
|
110
|
-
expect(replayed.error).toBeDefined()
|
|
111
|
-
expect(handler).toHaveBeenCalledOnce()
|
|
112
|
-
})
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
describe('response authenticity', () => {
|
|
116
|
-
it('client rejects a response whose signature does not verify', async () => {
|
|
117
|
-
const server = wrapMcpServer(echo, serverOptions())
|
|
118
|
-
const codec = createMcpClientCodec(clientOptions({ receive: { secretResolver: async () => 'WRONG-SECRET' } }))
|
|
119
|
-
|
|
120
|
-
const { raw } = await codec.encodeRequest({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: {} })
|
|
121
|
-
const responseRaw = await server(raw)
|
|
122
|
-
|
|
123
|
-
await expect(codec.decodeResponse(responseRaw)).rejects.toThrow(/verif/i)
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
describe('recipient binding (cross-server relay defense)', () => {
|
|
128
|
-
it('server rejects an envelope addressed to a different recipient', async () => {
|
|
129
|
-
const handler = vi.fn(echo)
|
|
130
|
-
const server = wrapMcpServer(handler, serverOptions({ selfAgentId: 'agent.server' }))
|
|
131
|
-
// a validly-signed request addressed to 'agent.other', relayed to this server
|
|
132
|
-
const misaddressed = await signEnvelopeHmac(
|
|
133
|
-
createEnvelope({
|
|
134
|
-
sender: 'agent.client',
|
|
135
|
-
recipient: 'agent.other',
|
|
136
|
-
intent: 'TASK',
|
|
137
|
-
content: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: {} }),
|
|
138
|
-
}),
|
|
139
|
-
SECRET,
|
|
140
|
-
'client-k1',
|
|
141
|
-
)
|
|
142
|
-
const responseRaw = await server(encodeEnvelope(misaddressed, 'json'))
|
|
143
|
-
|
|
144
|
-
// decode the error with a codec that matches the real server identity
|
|
145
|
-
const codec = createMcpClientCodec(clientOptions({ peerAgentId: 'agent.server' }))
|
|
146
|
-
const response = await codec.decodeResponse(responseRaw)
|
|
147
|
-
|
|
148
|
-
expect(handler).not.toHaveBeenCalled()
|
|
149
|
-
expect(response.error).toBeDefined()
|
|
150
|
-
})
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
describe('sender binding (response spoofing defense)', () => {
|
|
154
|
-
it('client rejects a validly-signed response whose sender is not the expected peer', async () => {
|
|
155
|
-
const codec = createMcpClientCodec(clientOptions({ peerAgentId: 'agent.server' }))
|
|
156
|
-
// a response signed with the shared secret but by a DIFFERENT agent id
|
|
157
|
-
const spoofed = await signEnvelopeHmac(
|
|
158
|
-
createEnvelope({ sender: 'agent.evil', recipient: 'agent.client', intent: 'RESULT', content: JSON.stringify({ jsonrpc: '2.0', id: 1, result: 'pwned' }) }),
|
|
159
|
-
SECRET,
|
|
160
|
-
'evil-k1',
|
|
161
|
-
)
|
|
162
|
-
const spoofedRaw = encodeEnvelope(spoofed, 'json')
|
|
163
|
-
|
|
164
|
-
await expect(codec.decodeResponse(spoofedRaw)).rejects.toThrow(/sender|peer/i)
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
describe('correlation binding (response substitution defense)', () => {
|
|
169
|
-
it('client rejects a response whose correlationId does not match the request', async () => {
|
|
170
|
-
const server = wrapMcpServer(echo, serverOptions())
|
|
171
|
-
const codec = createMcpClientCodec(clientOptions())
|
|
172
|
-
|
|
173
|
-
const { raw } = await codec.encodeRequest({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: {} })
|
|
174
|
-
const responseRaw = await server(raw)
|
|
175
|
-
|
|
176
|
-
// decode while asserting it answers a DIFFERENT request id
|
|
177
|
-
await expect(codec.decodeResponse(responseRaw, { expectCorrelationId: 'some-other-message-id' })).rejects.toThrow(/correlation/i)
|
|
178
|
-
})
|
|
179
|
-
})
|