feat(plugin, api): prevent plugin recursion on host model callbacks, enable targeted interceptor skipping

- Updated host model callback logic to skip originating plugin's interceptors during nested model executions.
- Added `SkipInterceptorPluginID` field to plugin API structs for controlling interceptor bypass behavior.
- Introduced supporting logic in host API handlers, plugin host registry, and callback contexts to identify and skip specific plugins.
- Enhanced unit tests across plugin host, API handlers, and execution paths to verify interceptor skipping behavior and plugin isolation.
- Revised documentation to clarify non-recursive behavior of host model callbacks and the use of `SkipInterceptorPluginID`.
This commit is contained in:
Luis Pater
2026-06-12 02:38:51 +08:00
parent 8e39db2ec7
commit 538e3416db
20 changed files with 474 additions and 85 deletions

View File

@@ -43,6 +43,8 @@ plugins:
`host-model-callback` declares the Management API capability and exposes a browser resource named `Host Model Callback`. The resource calls `host.model.execute` for non-streaming requests and `host.model.execute_stream` plus `host.model.stream_read` for streaming requests. It demonstrates explicit stream close with `host.model.stream_close` and an `implicit_close=true` option for RPC-scope host cleanup.
When the resource forwards its `host_callback_id`, CPA identifies the plugin that initiated the host model callback and skips that same plugin's interceptors for the nested execution. This makes host model callbacks non-recursive for the caller while allowing other plugins to intercept the nested request.
```yaml
plugins:
configs:

View File

@@ -43,6 +43,8 @@ plugins:
`host-model-callback` 声明 Management API 能力,并暴露名为 `Host Model Callback` 的浏览器资源。该资源在非流式请求中调用 `host.model.execute`,在流式请求中调用 `host.model.execute_stream``host.model.stream_read`。它演示了通过 `host.model.stream_close` 显式关闭流,也提供 `implicit_close=true` 用于演示 RPC 作用域结束时的宿主隐式清理。
当该资源转发自身收到的 `host_callback_id`CPA 会识别发起宿主模型回调的插件,并在嵌套模型执行中跳过同一个插件的拦截器。因此宿主模型回调不会递归调用发起插件自身,但其他已启用插件仍可拦截这次嵌套请求。
```yaml
plugins:
configs:

View File

@@ -115,6 +115,12 @@ By default, streaming mode explicitly closes the host-owned stream with `host.mo
When `implicit_close=true` is set, the plugin intentionally skips the explicit close call. CPA injects `host_callback_id` into the `management.handle` request, and this example forwards that callback ID to `host.model.execute_stream` so the host can close the stream when the `management.handle` RPC callback scope returns. This mode exists only to demonstrate host cleanup behavior; normal plugin code should explicitly close streams it opens.
## Recursion Guard
This example forwards the `host_callback_id` received from `management.handle` when it calls `host.model.execute` or `host.model.execute_stream`. CPA uses that callback scope to identify the plugin that initiated the host model callback and skips that same plugin's request, response, and stream interceptors for the nested model execution.
Host model callbacks are therefore not recursive for the caller. Other enabled plugins can still intercept the nested request.
## Billing and Usage
The callback uses the existing CPA model executor path. Usage collection, request accounting, and billing metadata are handled by the same executor and usage reporter path as normal proxied requests. The callback layer does not bill twice and does not create an additional usage record by itself.

View File

@@ -427,6 +427,9 @@ func executeOnce(opts runOptions) (pluginapi.HostModelExecutionResponse, error)
if errBody != nil {
return pluginapi.HostModelExecutionResponse{}, errBody
}
// Forward HostCallbackID so the host skips this plugin's interceptors on the
// nested model execution. Host model callbacks do not recursively call the
// originating plugin's interceptor chain.
result, errCall := callHost(pluginabi.MethodHostModelExecute, hostModelExecutionRequest{
HostModelExecutionRequest: pluginapi.HostModelExecutionRequest{
EntryProtocol: opts.EntryProtocol,
@@ -456,6 +459,9 @@ func executeStream(opts runOptions) (data streamPageData) {
data.Error = errBody.Error()
return data
}
// Forward HostCallbackID so the host skips this plugin's interceptors on the
// nested model execution. Host model callbacks do not recursively call the
// originating plugin's interceptor chain.
result, errCall := callHost(pluginabi.MethodHostModelExecuteStream, hostModelExecutionRequest{
HostModelExecutionRequest: pluginapi.HostModelExecutionRequest{
EntryProtocol: opts.EntryProtocol,