Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/content/docs/cli/harness-cli.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,23 @@ configures the runtime's gateway authorizer.
in `harness.json` (no key). Calling such a runtime requires your own
`Authorization: Bearer <user-pool JWT>` header — the CLI does not mint it.

### Calling OAuth remote agents from A2A Registry

When a harness enables the `agentkit_a2a` registry, the runtime dynamically
discovers agents through `SearchAgentCards` / `GetA2aAgent` and mounts them as
per-turn `remote_a2a_*` tools. If a matched remote AgentCard uses `apiKey`
auth, the runtime injects the declared header from `securitySchemes`. If it
uses `oauth2` with `clientCredentials.tokenUrl`, the runtime parses the
Volcengine Identity UserPool from the token URL, finds its
`MACHINE_TO_MACHINE` client, exchanges `client_credentials` for an access token,
and calls the remote agent's `message/send` / `tasks/get` with
`Authorization: Bearer <access_token>`.

Normally this only requires the harness runtime to have permission to call the
AgentKit A2A Registry and Identity OpenAPI. If the registry endpoint is custom
and cannot also serve Identity OpenAPI requests, set `REGISTRY_ID_ENDPOINT` or
`AGENTKIT_ID_ENDPOINT` to the Identity OpenAPI endpoint.

## `veadk harness invoke`

Invoke a **deployed** harness and print its output. The `url` and `key` are resolved from `harness.json` (written by `deploy`) by `--name`, so you need not pass them explicitly.
Expand Down
6 changes: 6 additions & 0 deletions docs/content/docs/cli/harness-cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ auth:
- 用户池、客户端、外部身份提供商(如飞书)在 **Identity 控制台**一次性建好,CLI 只**引用** `discovery_url` + `allowed_ids`,**不涉及任何 secret**。
- custom_jwt 部署后 `harness.json` 记录 `{url, runtime_id, auth_type, discovery_url, allowed_ids}`(无 key)。调用该 runtime 需自带 `Authorization: Bearer <用户池签发的 JWT>`,CLI 不代为获取。

### A2A Registry 调用 OAuth remote-agent

当 Harness 启用 `agentkit_a2a` registry 后,运行时会通过 `SearchAgentCards` / `GetA2aAgent` 动态发现并挂载 `remote_a2a_*` 工具。命中的远程 Agent 如果是 `apiKey` 鉴权,会按 AgentCard 的 `securitySchemes` 注入对应 header;如果是 `oauth2` 且声明了 `clientCredentials.tokenUrl`,运行时会自动从 token URL 解析火山引擎 Identity UserPool,查询其中的 `MACHINE_TO_MACHINE` client,使用 `client_credentials` 换取 access token,并以 `Authorization: Bearer <access_token>` 调用远程 Agent 的 `message/send` / `tasks/get`。

通常只需要确保 Harness Runtime 有权限调用 AgentKit A2A Registry 和 Identity OpenAPI。若 registry endpoint 使用了自定义域名且不能同时访问 Identity OpenAPI,可通过 `REGISTRY_ID_ENDPOINT` 或 `AGENTKIT_ID_ENDPOINT` 指定 Identity OpenAPI 地址。

## `veadk harness invoke`

调用一个**已部署**的 harness 并打印输出。`url` 与 `key` 默认按 `--name` 从 `harness.json`(由 `deploy` 写入)解析,无需显式传入。
Expand Down
114 changes: 114 additions & 0 deletions tests/a2a/test_registry_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from veadk.a2a.registry_client import (
AgentKitA2ARegistryConfig,
RegistryError,
_OAUTH_TOKEN_CACHE,
_agent_auth_headers,
_volc_sign_v4,
create_task,
Expand Down Expand Up @@ -68,6 +69,41 @@ def _agent_card() -> dict:
}


def _oauth_agent_card() -> dict:
token_url = (
"https://userpool-61597ac7-4bcb-4acf-a1d8-fdbfb95333ad."
"userpool.auth.id.cn-beijing.volces.com/oauth/token"
)
return {
"name": "Finance Policy Remote Agent",
"description": "Finance policy agent",
"version": "1.0.0",
"url": " https://oauth-agent.test/a2a/ ",
"security": [{"oauth2": []}],
"securitySchemes": {
"oauth2": {
"type": "oauth2",
"description": "OAuth2 client credentials flow",
"flows": {
"clientCredentials": {
"tokenUrl": f" `{token_url}` ",
"refreshUrl": f" `{token_url}` ",
"scopes": {},
}
},
}
},
"skills": [
{
"id": "finance-policy",
"name": "Finance policy",
"description": "Answer finance policy questions",
"tags": ["finance", "policy"],
}
],
}


@patch.dict(
"os.environ",
{
Expand Down Expand Up @@ -190,6 +226,84 @@ def test_create_task_gets_agent_and_sends_message(post: Mock):
assert "Authorization" not in serialized


@patch.dict(
"os.environ",
{
"AGENTKIT_ACCESS_KEY": "ak-test",
"AGENTKIT_SECRET_KEY": "sk-test",
},
clear=False,
)
@patch("veadk.a2a.registry_client.requests.post")
def test_create_task_gets_oauth_agent_token_and_sends_message(post: Mock):
_OAUTH_TOKEN_CACHE.clear()
card = _oauth_agent_card()
post.side_effect = [
_mock_response(
{
"ResponseMetadata": {"RequestId": "get-req"},
"Result": {
"Id": "agent-id",
"Status": "running",
"AgentCard": json.dumps(card),
},
}
),
_mock_response(
{
"ResponseMetadata": {"RequestId": "list-client-req"},
"Result": {"Data": [{"Uid": "m2m-client-id"}]},
}
),
_mock_response(
{
"ResponseMetadata": {"RequestId": "get-client-req"},
"Result": {"ClientSecret": "m2m-client-secret"},
}
),
_mock_response({"access_token": "oauth-access-token", "expires_in": 3600}),
_mock_response(
{
"result": {
"kind": "message",
"parts": [{"kind": "text", "text": "需要财务审批。"}],
}
}
),
]

result = create_task(
"Finance Policy Remote Agent",
"这笔支出是否需要审批?",
config=AgentKitA2ARegistryConfig(
space_id="space-test",
endpoint="https://open.volcengineapi.com/",
),
)

assert result["outcome"] == "success"
assert result["selected_agent"]["name"] == "Finance Policy Remote Agent"
assert result["response"]["text"] == "需要财务审批。"

assert post.call_args_list[0].kwargs["params"]["Action"] == "GetA2aAgent"
assert post.call_args_list[1].kwargs["params"]["Action"] == "ListUserPoolClients"
assert post.call_args_list[1].kwargs["params"]["Version"] == "2025-10-30"
assert post.call_args_list[2].kwargs["params"]["Action"] == "GetUserPoolClient"
assert post.call_args_list[2].kwargs["params"]["Version"] == "2025-10-30"
assert post.call_args_list[3].args[0].endswith("/oauth/token")
assert (
post.call_args_list[3].kwargs["headers"]["Authorization"].startswith("Basic ")
)
assert post.call_args_list[4].args[0] == "https://oauth-agent.test/a2a/"
assert post.call_args_list[4].kwargs["headers"]["Authorization"] == (
"Bearer oauth-access-token"
)

serialized = json.dumps(result, ensure_ascii=False)
assert "oauth-access-token" not in serialized
assert "m2m-client-secret" not in serialized


@patch.dict(
"os.environ",
{
Expand Down
Loading
Loading