Skip to content

[Bug]: 关闭 pushNotifications 后仍接受 inline 回调请求 #99

Description

Checklist

  • 我已经搜索过相关问题,但没有得到预期的帮助。
  • 最新版本中该错误尚未修复。
  • 请注意,如果您提交的Bug描述缺少相应的环境信息和最小可复现的demo,我们将很难复现和解决该问题,从而降低收到反馈的可能性,甚至该问题将被关闭。

🐞 问题详细描述

1. 问题概述

ReactAgent webhook 验收发现:默认 profile 的 Agent Card 正确声明 pushNotifications=false,固定 callback receiver也返回未启用响应;但调用方仍可在 SendMessage.params.pushNotificationConfig 中携带 callback 配置,请求返回 JSON-RPC result 并创建异步 Task,而不是在 Task 创建和 Agent 执行前拒绝。

代码核查确认,A2aPushNotificationCapabilityGate 当前只用于 Agent Card 和 callback receiver,没有用于 /a2a 的 inline pushconfig ingress。parser 仍会无条件保存 callback 配置并设置 returnImmediately=true,HTTP sender 和 config store 也不受push-notifications=false 控制。

这导致同一 runtime 对外出现不一致事实:Card 表示能力关闭、receiver 表示能力关闭,但 SendMessage 仍接受并启动 callback-mode异步执行。

2. 当前 runtime 代码实现

2.1 capability gate 能正确计算能力状态

文件:

service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/A2aPushNotificationCapabilityGate.java

public boolean isPushNotificationsEnabled() {
    return properties.isPushNotifications()
        && sender instanceof HttpPushNotificationSender
        && callbackStore != null
        && callbackHandler != null
        && !(callbackHandler instanceof NoOpA2aPushNotificationCallbackHandler);
}

该 gate 会检查部署开关和 callback 依赖是否完整。

2.2 gate 只用于 Card 和 callback receiver

当前使用点包括:

  • AgentCardController:决定 Card 中的 pushNotifications
  • A2aPushNotificationCallbackController:未启用时返回 501 Not Implemented

A2aJsonRpcController 没有注入或调用该 gate。

2.3 parser 无条件启用异步接受语义

文件:

service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/A2aJsonRpcParamsParser.java

if (inlinePushConfig != null && !inlinePushConfig.isJsonNull()) {
    builder.taskPushNotificationConfig(
        parseTaskPushNotificationConfig(inlinePushConfig,
            "params.pushNotificationConfig"));
    builder.returnImmediately(true);
}

这里没有检查 A2AProperties.isPushNotifications() 或 capability gate。

2.4 controller 只校验 URL policy

文件:

service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/A2aJsonRpcController.java

SendMessage 当前执行:

var params = A2aJsonRpcParamsParser.parseMessageSendParams(request.payload());
validateInlinePushNotificationConfig(params);
EventKind result = requestHandler.onMessageSend(params, ctx);

validateInlinePushNotificationConfig() 只调用:

A2aPushNotificationCallbackUrlPolicy.validateCallbackUrl(...);

只要 URL 满足信任策略,即使 capability 为 false,请求仍进入 SDK handler。

2.5 sender 和 config store 无条件装配

文件:

service/agent-service-app/src/main/java/com/openjiuwen/service/app/autoconfigure/A2AAutoConfiguration.java

当前无条件创建:

new InMemoryPushNotificationConfigStore();
new HttpPushNotificationSender(pushConfigStore);

这意味着 push-notifications=false 时,底层 inline config 存储和 HTTP sender 仍然存在,SDK 仍可进入 callback-mode 执行。

2.6 core 不应参与门控

日志显示请求已经进入 JiuwenCoreAgentHandler 和 ReActAgent,说明拒绝发生得太晚或完全缺失。是否允许 callback 是 A2A ingress
配置决策,agent-core-java 不知道也不应知道 pushNotifications 配置,本问题与 core 实现无关。

3. 当前实现与设计的冲突

对比项 文档预期 当前实现/测试表现
Agent Card 关闭时声明 false 符合
固定 callback receiver 关闭时不暴露或返回未启用 符合,返回 404/501
inline callback ingress 关闭时不应接受 callback-mode 请求 仍返回 result 并创建 Task
push config 存储 拒绝请求时无副作用 parser/SDK 仍可绑定配置
Agent 执行 门控拒绝应发生在业务执行前 已进入 ReactAgent/LLM
能力事实一致性 Card、receiver、sender ingress 使用统一门控 gate 未用于 SendMessage ingress

4. 建议修改方向

4.0 修改项关系说明

本问题的核心必改项只有一条:requestHandler.onMessageSend() 和 Task 创建之前,用统一 capability gate 拒绝关闭状态下的inline push config。其余示例用于代码组织、错误类型选择或防御性加强,不要求全部实施。

修改项 是否必须 关系说明
4.1 ingress 在 Task 创建前调用 capability gate 必须 修复本问题的核心,不可只修改 Card 或 receiver
InvalidParamsError 错误类型 可选实现 可以换成设计认可的 capability-disabled/unsupported A2A error,但必须返回 JSON-RPC error 且无副作用
4.2 给 gate 增加 requirePushNotificationsEnabled() 可选重构 与直接调用 isPushNotificationsEnabled() 二选一,用于减少重复判断
4.3 固定关闭时错误表面 必须确定契约 具体错误码可选,但开发、设计和测试必须一致
4.4 条件装配/NoOp sender 可选防御性方案 不能替代 8.1 ingress 门控;可在核心修复后进一步降低误投递风险
4.5 模块测试 必须 必须证明 handler、Task/config store 和 sender 均无副作用

推荐最小落地组合是:4.1 直接注入并调用现有 gate + 确定 4.3 错误表面 + 完成 4.5 测试

若希望进一步统一代码,可把 4.1 中的布尔判断重构为 4.2 的 requirePushNotificationsEnabled();两种写法不需要同时保留。4.4 的 NoOp sender 是额外防线,不是本问题通过验收的前置条件。

4.1 在 Task 创建前统一门控

建议 A2aJsonRpcController 或统一的 SendMessage callback validation service 使用A2aPushNotificationCapabilityGate.isPushNotificationsEnabled()

当请求包含 inline push config 且 gate 为 false 时,应在调用 requestHandler.onMessageSend() 前返回标准错误,确保无 Task、无配置、无Agent 执行和无 callback。

必改方向是把 capability gate 用于 A2aJsonRpcController 的 ingress。下面展示“直接调用现有 gate”的推荐最小示例:

public class A2aJsonRpcController {
    private final RequestHandler requestHandler;
    private final A2aPushNotificationCapabilityGate pushNotificationCapabilityGate;

    public A2aJsonRpcController(RequestHandler requestHandler,
            A2aPushNotificationCapabilityGate pushNotificationCapabilityGate) {
        this.requestHandler = requestHandler;
        this.pushNotificationCapabilityGate = pushNotificationCapabilityGate;
    }

    private void validateInlinePushNotificationConfig(MessageSendParams params) {
        if (params == null || params.configuration() == null
                || params.configuration().taskPushNotificationConfig() == null) {
            return;
        }

        if (!pushNotificationCapabilityGate.isPushNotificationsEnabled()) {
            throw new InvalidParamsError(
                    "Invalid params: push notification capability is not enabled");
        }

        A2aPushNotificationCallbackUrlPolicy.validateCallbackUrl(
                params.configuration().taskPushNotificationConfig());
    }
}

现有 controller 已捕获 A2AError 并转换为 JSON-RPC error,因此 InvalidParamsError 会在调用requestHandler.onMessageSend() 前返回,不创建 Task。若团队希望区分“参数非法”和“能力关闭”,也可新增 SDK 允许的capability-disabled/unsupported error;关键是保持标准 JSON-RPC error 且无副作用。

对应 unit test 应同时验证 handler 没有被调用:

@Test
void disabledPushCapabilityRejectsInlineConfigBeforeTaskCreation() {
    RequestHandler requestHandler = mock(RequestHandler.class);
    A2aPushNotificationCapabilityGate gate = mock(A2aPushNotificationCapabilityGate.class);
    when(gate.isPushNotificationsEnabled()).thenReturn(false);
    A2aJsonRpcController controller = new A2aJsonRpcController(requestHandler, gate);

    ResponseEntity<?> response = controller.handleJsonRpc(
            sendMessageWithInlinePushConfig(), servletRequest());

    assertThat(response.getBody().toString()).contains("error").contains("-32602");
    verifyNoInteractions(requestHandler);
}

测试辅助方法名需按现有 A2aJsonRpcControllerTest 调整;核心断言是 verifyNoInteractions(requestHandler)

4.2 统一 Card、receiver 和 ingress 的能力事实

同一个 capability 判定应同时约束:

  • Agent Card 是否声明 pushNotifications=true
  • 固定 callback receiver 是否接受请求;
  • SendMessage 是否接受 inline push config;
  • sender 是否允许实际投递。

避免出现 Card false 但发送能力仍可使用,或 Card true 但入口不可用的状态。

以下为可选重构:为避免以后不同入口各自复制判断,可以给 gate 增加统一校验方法。采用该写法后,4.1 示例中的显式布尔判断应替换掉,不需要两段判断同时保留:

public void requirePushNotificationsEnabled() {
    if (!isPushNotificationsEnabled()) {
        throw new InvalidParamsError(
                "Invalid params: push notification capability is not enabled");
    }
}

controller 使用:

if (hasInlinePushNotificationConfig(params)) {
    pushNotificationCapabilityGate.requirePushNotificationsEnabled();
    A2aPushNotificationCallbackUrlPolicy.validateCallbackUrl(
            params.configuration().taskPushNotificationConfig());
}

receiver 仍可根据 HTTP endpoint 契约把 gate false 映射为 404/501,不必强行与 JSON-RPC ingress 使用相同 HTTP status。

4.3 明确关闭时的标准错误表面

建议在设计和实现中固定为 invalid params、capability disabled 或等价 unsupported error。HTTP 可以保持 JSON-RPC over HTTP 的正常传输语义,关键是 response 必须包含 JSON-RPC error,并且没有业务副作用。

4.4 评估条件装配或 NoOp sender

可选择让 sender/config store 条件装配,或者保持 bean 存在但在统一 gate 下拒绝使用。无论选择哪种实现,不能只依赖 Agent Card隐藏能力。

本项为可选防御性方案。如果希望从 bean 层进一步避免关闭状态下存在真实 HTTP sender,可以使用条件装配并提供 NoOp 实现。以下为示意代码:

@Bean
@ConditionalOnProperty(
        prefix = "openjiuwen.service.a2a",
        name = "push-notifications",
        havingValue = "true")
@ConditionalOnMissingBean(PushNotificationSender.class)
public PushNotificationSender a2aHttpPushNotificationSender(
        PushNotificationConfigStore pushConfigStore) {
    return new HttpPushNotificationSender(pushConfigStore);
}

@Bean
@ConditionalOnMissingBean(PushNotificationSender.class)
public PushNotificationSender a2aNoOpPushNotificationSender() {
    return (event, task) -> {
        // capability disabled: no delivery
    };
}

当前 gate 已要求 sender 是 HttpPushNotificationSender,因此 NoOp sender 会自然使 Card capability 为 false。需要注意 Spring条件 bean 的声明顺序和 @ConditionalOnMissingBean 评估时机,实际实现可以用独立配置类或显式 NoOp class 避免装配歧义。

bean 条件装配只能作为防御性措施,不能替代 ingress 拒绝:否则请求仍可能被 parser/SDK 接受并创建永远不会回调的异步 Task。

4.5 补充 runtime 模块测试

建议覆盖:

  • 配置 false + inline config:拒绝且不调用 executor;
  • 配置 true 但 handler/store/sender 不完整:Card false 且 ingress 拒绝;
  • 完整 callback profile:Card true 且合法 inline config accepted;
  • 不可信 URL:能力开启时仍按 trust policy 拒绝;
  • gate false 时 config store 无新增记录、sender 无调用。

详细的环境信息描述

本次定位代码版本:

agent-runtime-java: develop / 6afd64f
agent-core-java:    730 / 921b4bde

其他辅助信息

版本信息

感谢您的贡献 🎉!

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions