mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
63 lines
2.7 KiB
Go
63 lines
2.7 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/url"
|
|
|
|
"github.com/Autumn-27/artex/egress"
|
|
"github.com/Autumn-27/artex/intercept"
|
|
actool "github.com/Autumn-27/norma/tool"
|
|
)
|
|
|
|
// withWebFetch returns tools plus the WebFetch tool of an agent session. Every
|
|
// agent kind builds WebFetch here, one tool per run, so that its configuration
|
|
// can depend on the run: every redirect hop goes through the target gate
|
|
// (intercept.RedirectCheck), and every connection through the pinned dialer
|
|
// (intercept.PinnedDialer), on the client that uses proxy and on the direct
|
|
// retry client alike. ctx is the context of the run: when it carries a target
|
|
// scope the dialer never connects without one, and when that scope's gate is
|
|
// on the direct retry is turned off for the run, since it would leave without
|
|
// the proxy. The tool is added after the session's own tools, where agentcore
|
|
// adds it, so the tool list keeps its order. caCert is the recording proxy's
|
|
// CA, trusted to verify the HTTPS it re-signs. Each call goes through the shared
|
|
// egress governor (see governedFetch). tools is not modified.
|
|
func withWebFetch(ctx context.Context, tools []actool.CoreTool, proxy, caCert string) []actool.CoreTool {
|
|
scope, scoped := intercept.TargetScopeFromContext(ctx)
|
|
gated := scoped && scope.Enabled != nil && scope.Enabled()
|
|
webFetch := actool.NewWebFetch(actool.WebFetchConfig{Proxy: proxy, CACert: caCert, CheckRedirect: intercept.RedirectCheck("WebFetch"),
|
|
DialContext: intercept.PinnedDialer("WebFetch", scoped), DisableDirectRetry: gated})
|
|
return append(tools[:len(tools):len(tools)], governedFetch{webFetch, egressGovernor()})
|
|
}
|
|
|
|
// egressGovernor is the governor of WebFetch; a test may pin it.
|
|
var egressGovernor = egress.Shared
|
|
|
|
// governedFetch runs each WebFetch call as one send under the egress governor
|
|
// (off by default; then the call runs unchanged). Under a noise profile it
|
|
// paces the call by the rate, jitter and concurrency of the URL's host, and
|
|
// nothing else: WebFetch calls are neither coalesced nor cached, since each
|
|
// tool carries its own proxy and redirect and dial gate (see egress.Do).
|
|
// Norma owns the HTTP client, so redirect hops are not paced separately and a
|
|
// 429/503 is not retried.
|
|
type governedFetch struct {
|
|
actool.CoreTool
|
|
g *egress.Governor
|
|
}
|
|
|
|
func (f governedFetch) Call(ctx context.Context, input json.RawMessage, tc *actool.ToolContext) (actool.Result, error) {
|
|
var in struct {
|
|
URL string `json:"url"`
|
|
}
|
|
if json.Unmarshal(input, &in) != nil {
|
|
return f.CoreTool.Call(ctx, input, tc)
|
|
}
|
|
u, err := url.Parse(in.URL)
|
|
if err != nil || u.Host == "" {
|
|
return f.CoreTool.Call(ctx, input, tc) // the tool reports the bad URL
|
|
}
|
|
return egress.Do(f.g, ctx, u, func() (actool.Result, error) {
|
|
return f.CoreTool.Call(ctx, input, tc)
|
|
})
|
|
}
|