AgentRun 的 Agent 运行时、沙箱、模型代理等服务统一通过 HTTP/HTTPS 协议对外开放。本文以 Agent 运行时为例,介绍两种访问方式:直接 HTTP 访问(可配合自定义凭证)和 AccessKey 签名访问(适用于 RAM 用户及程序化调用场景)。
请求及返回结果均使用 UTF-8 字符集编码。
前提条件
已部署 Agent 运行时,并获取 Agent 运行时名称和端点名称。
如需签名访问,已通过创建 AccessKey获取 AccessKey ID 和 AccessKey Secret。
HTTP API 访问
以 Agent 运行时为例,向已部署的 Agent 发起 OpenAI 兼容的 Chat Completions 请求:
curl "https://12**********.agentrun-data.cn-hangzhou.aliyuncs.com/agent-runtimes/your-agent-name/endpoints/Default/invocations/openai/v1/chat/completions" \
-X POST \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user", "content": "写一段代码,查询现在是几点?"}],
"stream": true
}'URL 结构说明
请求 URL 由以下部分组成:
路径片段 | 含义 |
| 阿里云账号 UID |
| AgentRun 系统域名,其中 |
| 固定路径,表示资源类型为 Agent 运行时 |
| 已部署的 Agent 运行时名称 |
| 固定路径。 |
| Agent Server 提供的有效路由,根据 Agent Server 的实际实现而定 |
凭证认证
若 Agent 运行时配置了访问凭证,需在请求头中携带对应的认证信息。AgentRun 支持多种凭证形式:
API Key:在请求头中携带配置的 Key。例如:-H "X-API-Key: ********"JWT:需由调用方自行向 Issuer 获取 JWT。AgentRun 通过 JWKS 公钥验证 JWT 的合法性,不负责签发 JWT。
签名访问 HTTP API
AgentRun 支持通过阿里云标准签名方式访问资源,适用于 RAM 用户及需要 AccessKey (AK/SK) 鉴权的程序化调用场景。
curl "https://12**********-ram.agentrun-data.cn-hangzhou.aliyuncs.com/agent-runtimes/your-agent-name/endpoints/Default/invocations/openai/v1/chat/completions" \
-X POST \
-H "Content-Type: application/json" \
-H "Agentrun-Authorization: AGENTRUN4-HMAC-SHA256 Credential=<AccessKeyId>/<SignDate>/<SignRegion>/agentrun/aliyun_v4_request, SignedHeaders=<AdditionalHeadersVal>, Signature=<SignatureVal>" \
-d '{
"messages": [{"role": "user", "content": "写一段代码,查询现在是几点?"}],
"stream": true
}'与直接 HTTP 访问相比,签名访问有以下差异:
项目 | 直接 HTTP 访问 | 签名访问 |
域名 |
|
|
鉴权方式 | 可选自定义凭证(API Key、JWT 等) |
|
适用场景 | 已配置凭证的 Agent 对外调用 | RAM 用户、AK/SK 程序化调用 |
Agentrun-Authorization 请求头格式
AGENTRUN4-HMAC-SHA256 Credential=<AccessKeyId>/<date>/<region>/<product>/aliyun_v4_request,SignedHeaders=<SignedHeaders>,Signature=<Signature>重要约定
签名算法标识为
AGENTRUN4-HMAC-SHA256。请求体参与签名的方式为
UNSIGNED-PAYLOAD:规范化请求最后一行及请求头x-acs-content-sha256均为字面量UNSIGNED-PAYLOAD,不对请求体做 SHA256 十六进制摘要。授权头字段名为
Agentrun-Authorization(不是Authorization)。密钥派生使用前缀
aliyun_v4,对 AccessKey Secret 做多段 HMAC-SHA256 派生得到 SigningKey,不能直接用 AccessKey Secret 对 StringToSign 做 HMAC。
签名机制
采用 AccessKey ID 和 AccessKey Secret(及可选的 STS SecurityToken)对 HTTP/HTTPS 请求签名。服务端按相同规则重算签名并与请求中的授权信息比对,以校验调用方身份与请求完整性。
步骤一:构造规范化请求
构造规范化请求 (CanonicalRequest) 的伪代码如下:
CanonicalRequest =
HTTPRequestMethod + '\n' +
CanonicalURI + '\n' +
CanonicalQueryString + '\n' +
CanonicalHeaders + '\n' +
SignedHeaders + '\n' +
RequestPayloadPlaceholder其中 RequestPayloadPlaceholder 固定为字符串 UNSIGNED-PAYLOAD(与请求是否有 body 无关)。
HTTPRequestMethod
HTTP 方法名全大写,例如 GET、POST。
CanonicalURI
取 URL 的路径部分(host 与 ? 查询串之间的片段,含首字符 /)。若为空则使用 /。
标准文档中对路径分段做 RFC 3986 百分号编码。实际调用方应保证 path 与网关期望一致,参考实现以 URL 解析结果为准。
CanonicalQueryString
无查询参数时,CanonicalQueryString 为空字符串
""。有查询参数时:
将所有 query 参数按参数名字典序升序排列。
对每个参数的名称与值分别做百分号编码后,用
=连接。若值为null、空串或缺失,仍输出编码后的key=(等号右侧为空)。多个参数之间用
&连接,不在整个字符串前加?。
百分号编码规则(与 Python urllib.parse.quote(safe='') 再 replace('%7E','~') 一致):使用 UTF-8 字节序列编码,encodeURIComponent 语义下再将 %7E 还原为 ~。
CanonicalHeaders
仅包含同时满足以下条件的请求头(不含 Agentrun-Authorization 授权头本身):
请求头名为
host、content-type,或以x-acs-为前缀。对应取值非
null、非缺失。
构造步骤:
将参与签名的头名统一转为小写。
将头值去除首尾空白 (Trim)。
若存在同名小写头(重复头):
除
host外,多个值按出现顺序用英文逗号,拼接。host实际请求中应只保留一个,避免歧义。
按头名字典序升序排列,对每个头输出一行:
lowercase_name:trimmed_value\n。多行头直接拼接成 CanonicalHeaders 字符串(每行末尾都有\n,含最后一行)。
参考实现中参与签名的头通常包括:
请求头 | 值 |
| 请求域名 |
| UTC 时间,格式为 ISO 8601,例如 |
| 固定为 |
| 使用 STS 时需要携带 |
若业务需要将 content-type 纳入签名,须确保该请求头存在且值非空,签名过程会自动将其纳入规范化头。
SignedHeaders
取 CanonicalHeaders 中实际参与签名的头名(小写),按字典序升序排列,用英文分号 ; 连接为单行字符串。
示例值:
host;x-acs-content-sha256;x-acs-date;x-acs-security-tokenRequestPayloadPlaceholder
固定为:
UNSIGNED-PAYLOAD请求头 x-acs-content-sha256 的值也必须为 UNSIGNED-PAYLOAD,与上述占位一致。
步骤二:构造待签名字符串
HashedCanonicalRequest
对步骤一得到的完整 CanonicalRequest 字符串(UTF-8 编码)做 SHA256 哈希,并转为小写十六进制字符串:
HashedCanonicalRequest = HexEncode( SHA256( CanonicalRequest ) )StringToSign
StringToSign =
SignatureAlgorithm + '\n' +
HashedCanonicalRequest其中 SignatureAlgorithm 固定为 AGENTRUN4-HMAC-SHA256。
示例:
AGENTRUN4-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259第二行为示例哈希值,以实际计算为准。
步骤三:计算签名
派生 SigningKey
使用以下常量:
SIGN_PREFIX = "aliyun_v4"日期戳
date:取签名时刻 UTC 的YYYYMMDD(与x-acs-date对应同一天),例如20231026。
派生过程(均为 HMAC-SHA256,数据为 UTF-8 字符串,密钥为二进制上一段输出):
k0 = HMAC-SHA256( key = UTF8(SIGN_PREFIX + AccessKeySecret), data = date )
k1 = HMAC-SHA256( key = k0, data = region )
k2 = HMAC-SHA256( key = k1, data = product )
SigningKey = HMAC-SHA256( key = k2, data = SIGN_PREFIX + "_request" )region:例如cn-hangzhou,须与网关约定一致。product:固定为agentrun。
计算 Signature
Signature = HexEncode( HMAC-SHA256( key = SigningKey, data = StringToSign ) )HexEncode 为小写十六进制。
此处使用的是 SigningKey(经过多段派生的密钥),不是原始 AccessKey Secret。
步骤四:将签名添加到请求中
构造 Agentrun-Authorization 请求头:
AGENTRUN4-HMAC-SHA256 Credential=<AccessKeyId>/<date>/<region>/<product>/aliyun_v4_request,SignedHeaders=<SignedHeaders>,Signature=<Signature>Credential中<date>与步骤三中派生密钥使用的YYYYMMDD日期一致。SignedHeaders与步骤一中的 SignedHeaders 字符串一致。Signature为步骤三计算得到的小写十六进制串。
完整请求中至少应携带以下头部字段:
请求头 | 说明 |
| 请求域名 |
| UTC 时间,ISO 8601 格式 |
| 固定为 |
| 签名授权头 |
| 使用 STS 临时凭证时必须携带 |
与标准 ACS3-HMAC-SHA256 的对比
AgentRun 的签名机制基于阿里云 V4 签名,但存在以下差异:
项目 | 标准 ACS3-HMAC-SHA256 | AgentRun AGENTRUN4-HMAC-SHA256 |
算法名 |
|
|
body 摘要 |
| 固定为 |
CanonicalRequest 最后一行 | body 哈希或空 body 的哈希 | 固定为 |
StringToSign |
| 相同形式,但算法名与 CanonicalRequest 内容不同 |
签名密钥 | 常用 Secret 直接 HMAC |
|
授权头 |
|
|
Credential |
|
|
签名代码示例
以下为 Node.js 参考实现,可用于生成 Agentrun-Authorization 及配套请求头。
const crypto = require('crypto');
const SIGN_PREFIX = 'aliyun_v4';
const ALGORITHM = 'AGENTRUN4-HMAC-SHA256';
function percentEncode(value) {
if (value == null) return '';
return encodeURIComponent(value).replace(/%7E/g, '~');
}
function hmacSha256(key, data) {
const hmac = crypto.createHmac('sha256', key);
hmac.update(data, 'utf8');
return hmac.digest();
}
function getSigningKey(secret, product, region, date) {
const sc = SIGN_PREFIX + secret;
const dateKey = hmacSha256(Buffer.from(sc, 'utf8'), date);
const regionKey = hmacSha256(dateKey, region);
const productKey = hmacSha256(regionKey, product);
return hmacSha256(productKey, SIGN_PREFIX + '_request');
}
function getSignedHeaders(headers) {
const headerSet = new Set();
for (const [key, value] of Object.entries(headers)) {
const lowerKey = key.toLowerCase();
if (value != null && (lowerKey.startsWith('x-acs-') || lowerKey === 'host' || lowerKey === 'content-type')) {
headerSet.add(lowerKey);
}
}
return Array.from(headerSet).sort();
}
function buildCanonicalizedResource(query) {
if (!query || Object.keys(query).length === 0) return '';
const parts = [];
for (const key of Object.keys(query).sort()) {
const value = query[key];
if (value != null && value !== '') {
parts.push(`${percentEncode(key)}=${percentEncode(value)}`);
} else {
parts.push(`${percentEncode(key)}=`);
}
}
return parts.join('&');
}
function buildCanonicalizedHeaders(headers) {
const newHeaders = {};
const processedKeys = new Set();
for (const [key, value] of Object.entries(headers)) {
const lowerKey = key.toLowerCase();
if (value != null) {
if (!processedKeys.has(lowerKey) || lowerKey === 'host') {
processedKeys.add(lowerKey);
newHeaders[lowerKey] = String(value).trim();
} else {
newHeaders[lowerKey] = newHeaders[lowerKey] + ',' + String(value).trim();
}
}
}
const signedHeaders = getSignedHeaders(headers);
let result = '';
for (const h of signedHeaders) {
result += `${h}:${newHeaders[h]}\n`;
}
return result;
}
/**
* 生成 AgentRun 签名头
*
* @param {Object} options
* @param {string} options.url - 请求 URL
* @param {string} [options.method='GET'] - HTTP 方法
* @param {string} [options.accessKeyId] - AccessKey ID
* @param {string} [options.accessKeySecret] - AccessKey Secret
* @param {string} [options.securityToken] - Security Token (STS)
* @param {string} [options.region='cn-hangzhou'] - Region
* @param {string} [options.product='agentrun'] - Product
* @param {Buffer|Uint8Array} [options.body] - 请求体
* @param {Date} [options.signTime] - 签名时间,默认当前 UTC
* @returns {Object} 包含 host, x-acs-date, x-acs-content-sha256, Agentrun-Authorization 等
*/
function getAgentrunSignedHeaders(options = {}) {
const {
url,
method = 'GET',
accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID,
accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
securityToken = process.env.ALIBABA_CLOUD_SECURITY_TOKEN,
region = 'cn-hangzhou',
product = 'agentrun',
body,
signTime,
} = options;
if (!accessKeyId || !accessKeySecret) {
throw new Error('Access Key ID and Secret are required');
}
const parsed = new URL(url);
const host = parsed.host;
const pathname = parsed.pathname || '/';
const queryParams = {};
if (parsed.search) {
for (const [k, v] of parsed.searchParams) {
queryParams[k] = v;
}
}
const now = signTime ? new Date(signTime) : new Date();
const timestamp = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
const headers = {
host,
'x-acs-date': timestamp,
'x-acs-content-sha256': 'UNSIGNED-PAYLOAD',
};
if (securityToken) {
headers['x-acs-security-token'] = securityToken;
}
const hashPayload = 'UNSIGNED-PAYLOAD';
const signingKey = getSigningKey(accessKeySecret, product, region, date);
const canonicalUri = pathname || '/';
const canonicalizedResource = buildCanonicalizedResource(queryParams);
const canonicalizedHeaders = buildCanonicalizedHeaders(headers);
const signedHeaders = getSignedHeaders(headers);
const signedHeadersStr = signedHeaders.join(';');
const stringToSignFirst =
method.toUpperCase() +
'\n' +
canonicalUri +
'\n' +
canonicalizedResource +
'\n' +
canonicalizedHeaders +
'\n' +
signedHeadersStr +
'\n' +
hashPayload;
const hashHex = crypto.createHash('sha256').update(stringToSignFirst, 'utf8').digest('hex');
const stringToSignFinal = ALGORITHM + '\n' + hashHex;
const signature = hmacSha256(signingKey, stringToSignFinal).toString('hex');
const authHeader =
`${ALGORITHM} Credential=${accessKeyId}/${date}/${region}/${product}/` +
`${SIGN_PREFIX}_request,SignedHeaders=${signedHeadersStr},Signature=${signature}`;
headers['Agentrun-Authorization'] = authHeader;
return headers;
}
module.exports = { getAgentrunSignedHeaders };使用示例
以下示例展示如何使用上述签名函数发起带签名的请求:
const https = require('https');
const { getAgentrunSignedHeaders } = require('./agentrun-sign');
const url = 'https://12345678901234-ram.agentrun-data.cn-hangzhou.aliyuncs.com/agent-runtimes/my-agent/endpoints/Default/invocations/openai/v1/chat/completions';
const body = JSON.stringify({
messages: [{ role: 'user', content: '你好' }],
stream: false,
});
const signedHeaders = getAgentrunSignedHeaders({
url,
method: 'POST',
accessKeyId: process.env.ALIBABA_CLOUD_ACCESS_KEY_ID,
accessKeySecret: process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
region: 'cn-hangzhou',
product: 'agentrun',
});
const parsed = new URL(url);
const options = {
hostname: parsed.hostname,
path: parsed.pathname,
method: 'POST',
headers: {
...signedHeaders,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => { console.log(JSON.parse(data)); });
});
req.write(body);
req.end();