feat(backthink,browser): 自排工具面 C1-3 + 浏览器两处偏离收口(X2+X3 同文件一并落)
X2 C1-3(自排工具面与提示词): - request_think(thinktool.go,照 transcode/tool.go 范式)+ reason 必填/urgency 闭集 + 双守门 (安全闸 + owner 判定:群聊/陌生人一律拒) - **成对**进两张唤醒态允许集(quiet + awake;CAPACITY_TOOL_SETS 是整体替换) - 提示词抽共用段落 + 自排前缀优先(不枚举工具清单);面板加 self_wake 标签与筛选轴 - 4 个变异自检(拿掉 owner 判定/只加一张表/放宽 urgency/前端标签恒等) X3 收口(与 X2 共用 wire_services.go ⇒ 一并落): - 删掉第三份 SSRF 实现(guard.go),改由 io.ValidateRequestURL 唯一判定 + 薄门面(保两个错误身份) - 浏览器会话挂进 a.close(真机:真 Chrome 被关、无残留、生产未扰动) - a.wireBrowser() 归位到 wire_runtime.go,删掉 ioToolOptions 的临时挂点 - 5 个变异自检 顺带补提交 C1-2 漏掉的 internal/config/runtimeconfig/backthink_test.go(ed50053 漏 add)。
This commit is contained in:
1 parent
2bf3b536dd
commit
2d211c2d16
20 files changed
+1568
-176
No files matched your search
@@ -14,10 +14,27 @@ import (
|
||||
// Use errors.Is to test for it.
|
||||
var ErrSSRF = errors.New("io: disallowed url")
|
||||
|
||||
// ValidateRequestURL is the exported form of validateRequestURL: the single SSRF
|
||||
// policy for every code path that fetches a caller-supplied URL on the server's
|
||||
// behalf. It is exported because a second implementation of the same policy grew
|
||||
// outside this package — `internal/capability/browser` carried its own copy
|
||||
// (written when this function was unexported and that capability was unreachable
|
||||
// from the slice being implemented), which is what design/28 §4 forbids
|
||||
// ("reuse the existing io/ssrf allowlist, do not add a third implementation").
|
||||
// That copy is gone and the browser capability calls this function instead, so
|
||||
// the policy table (scheme check → allowlist → literal-IP deny →
|
||||
// resolve-and-deny) lives in one place again. Call sites inside this package
|
||||
// keep using the unexported name.
|
||||
func ValidateRequestURL(ctx context.Context, rawURL string, allowed []string) (*url.URL, error) {
|
||||
return validateRequestURL(ctx, rawURL, allowed)
|
||||
}
|
||||
|
||||
// validateRequestURL parses rawURL and refuses SSRF-prone targets: it returns an
|
||||
// error wrapping ErrSSRF for loopback/private/link-local/unspecified addresses
|
||||
// and for localhost hostnames, unless the host (or hostname) is on the explicit
|
||||
// allowlist. A hostname that resolves to any denied address is also refused.
|
||||
//
|
||||
// 内部调用点沿用未导出名;对外只有 ValidateRequestURL 一个出口。
|
||||
func validateRequestURL(ctx context.Context, rawURL string, allowed []string) (*url.URL, error) {
|
||||
if strings.TrimSpace(rawURL) == "" {
|
||||
return nil, fmt.Errorf("io: url is required")
|
||||
@@ -75,14 +92,40 @@ func isHostAllowed(host string, allowed []string) bool {
|
||||
return false
|
||||
}
|
||||
for _, a := range allowed {
|
||||
if strings.EqualFold(hostOnly(a), h) {
|
||||
if strings.EqualFold(allowEntryHost(a), h) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// allowEntryHost returns the hostname an allowlist entry stands for. Entries are
|
||||
// written by hand (panel config, tool options), so all three natural spellings
|
||||
// are accepted: `host`, `host:port` and a full URL.
|
||||
//
|
||||
// URL entries used to be honoured only by the browser-side copy of this policy
|
||||
// (now deleted); folding that capability in here is what keeps the collapse into
|
||||
// one implementation from silently weakening an existing allowlist — an entry
|
||||
// spelled as a URL would otherwise stop matching after the merge. A bare IPv6
|
||||
// literal (`::1`) is also returned as-is: the host:port split must not cut it.
|
||||
func allowEntryHost(entry string) string {
|
||||
raw := strings.TrimSpace(entry)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if u, err := url.Parse(raw); err == nil && u.Host != "" {
|
||||
if h := u.Hostname(); h != "" {
|
||||
return h
|
||||
}
|
||||
}
|
||||
return hostOnly(raw)
|
||||
}
|
||||
|
||||
// hostOnly strips the port (and any IPv6 brackets) from a host:port string.
|
||||
//
|
||||
// A **bare** IPv6 literal (`::1`, no brackets) is returned unchanged: the port
|
||||
// split would otherwise cut it at its last colon, and callers do pass
|
||||
// already-hostname-only strings (isHostAllowed normalizes its argument too).
|
||||
func hostOnly(hostport string) string {
|
||||
h := strings.TrimSpace(hostport)
|
||||
if h == "" {
|
||||
@@ -94,6 +137,9 @@ func hostOnly(hostport string) string {
|
||||
}
|
||||
return h
|
||||
}
|
||||
if net.ParseIP(h) != nil {
|
||||
return h
|
||||
}
|
||||
if i := strings.LastIndex(h, ":"); i >= 0 {
|
||||
return h[:i]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user