Why: F13 - the squad factory read the default account with an empty project, pairing the run user's model name with another account's endpoint and credentials; direct and squad paths disagreed on reasoning split and route contract. Evidence: app/runtime/squadadapter tests pass (verify-u10b.json); run-scoped snapshot, project override, profile override and fail-closed cases added with synthetic keys. Impact: squad execution now uses the same effective-config rules as the direct path; default setups unchanged. Refs: feature/agent-stack-audit-remediation-b1 U10
150 lines
5.7 KiB
Go
150 lines
5.7 KiB
Go
// 小队 agent-native 内核工厂的装配(组合根注入 squadadapter.AgentNativeOptions)。
|
|
//
|
|
// 配置解析的硬约束(KTD3 / 审计 F13):内核用的模型、端点、密钥必须整条来自
|
|
// 「本次运行」的生效快照——rc.Settings() 已按 run.Owner + run.ProjectID 合并,
|
|
// 画像模型覆盖叠加其上,per-route 规则(reasoning split / 受限 EOF 契约)由
|
|
// runtime.OpenRouteClient 与直连路径同一函数判定。不得在这里按默认账号重新
|
|
// 解析:那会把一个用户的模型名和另一个账号的端点/密钥拼在一起。
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"agent-native/runtime/checkpoint"
|
|
agentllm "agent-native/runtime/llm"
|
|
agentmcp "agent-native/runtime/mcp"
|
|
"agent-native/runtime/policy"
|
|
agenttools "agent-native/runtime/tools"
|
|
nativeadp "agent-squad/adapters/native"
|
|
"agent-system-backend/internal/runtime"
|
|
"agent-system-backend/internal/settings"
|
|
"agent-system-backend/internal/squadadapter"
|
|
"agent-system-backend/internal/store"
|
|
"agent-system-backend/internal/tools"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
// squadNativeDeps 是内核工厂的依赖。mcp 与 pluginMCP 取函数而非值:插件宿主在
|
|
// Run 晚些时候才装配,工厂必须按调用时读,不能把启动期的空值固化进去。
|
|
type squadNativeDeps struct {
|
|
// set 提供回落快照:宿主没有 Settings 端口时(骨架/旧宿主)才用默认账号。
|
|
set *settings.Service
|
|
// secrets 是受管密钥来源(Tavily 池)。
|
|
secrets activeSecretSource
|
|
// skillsRoot 是交付配方技能根。
|
|
skillsRoot string
|
|
trace func(name string, attrs map[string]string) func()
|
|
mcp func(raw, overrides string) *agentmcp.Manager
|
|
pluginMCP func() []core.PluginMCPServer
|
|
}
|
|
|
|
// squadAgentNativeFactory 返回注入 squadadapter.AgentNativeOptions 的工厂。
|
|
// 主模型未配置时返回未启用(不阻塞产品);其余装配对每次运行重新解析。
|
|
func squadAgentNativeFactory(deps squadNativeDeps) func(squadadapter.Ctx) (nativeadp.Config, bool) {
|
|
return func(rc squadadapter.Ctx) (nativeadp.Config, bool) {
|
|
// 当前运行的用户/项目快照;宿主无 Settings 端口时才回落默认账号。
|
|
effNow, ok := runtime.RunEffectiveSettings(rc)
|
|
if !ok {
|
|
if deps.set == nil {
|
|
return nativeadp.Config{}, false
|
|
}
|
|
effNow = deps.set.Effective(store.OwnerDefault, "")
|
|
}
|
|
// 画像模型覆盖走同一条解析规则(Owner 决定密钥层归属)。
|
|
route := runtime.ResolveChatModel(runtime.RunProfile(rc), effNow)
|
|
if route.Model == "" || route.Base == "" {
|
|
return nativeadp.Config{}, false
|
|
}
|
|
mode := "default"
|
|
if src, ok := rc.(interface{ PermissionMode() string }); ok {
|
|
mode = src.PermissionMode()
|
|
}
|
|
cfg := nativeadp.Config{
|
|
Client: runtime.OpenRouteClient(route, effNow),
|
|
DefaultModel: route.Model,
|
|
MaxTurns: 0,
|
|
Timeout: 0,
|
|
Subagent: squadadapter.SubagentFactory(rc),
|
|
RepeatGuard: true,
|
|
BeforeTool: func(call agentllm.ToolCall) error {
|
|
effect := policy.Decide(policy.Request{
|
|
Mode: policy.Normalize(mode), Name: call.Name, Args: call.Arguments,
|
|
Traits: policy.TraitsFor(call.Name),
|
|
}, nil)
|
|
if effect == policy.EffectDeny {
|
|
return fmt.Errorf("计划模式拒绝了工具 %s", call.Name)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
cfg.Stream = runtime.GuardProjectStream(rc, "", cfg.Client.Stream)
|
|
var keys []string
|
|
if deps.secrets != nil {
|
|
keys = tavilyKeyPool(deps.secrets, effNow.GetString("tools.tavily.apiKeys", ""))
|
|
}
|
|
if len(keys) > 0 {
|
|
cfg.Search = agenttools.NewTavilyProvider(effNow.GetString("tools.tavily.endpoint", ""), keys)
|
|
}
|
|
if effNow.GetBool("runtime.exec", true) {
|
|
cfg.Exec = tools.InterpreterExecConfig()
|
|
}
|
|
if st2, err := os.Stat(deps.skillsRoot); err == nil && st2.IsDir() {
|
|
cfg.SkillsRoots = []string{deps.skillsRoot}
|
|
}
|
|
rawMCP := strings.TrimSpace(effNow.GetString("tools.mcp.servers", ""))
|
|
if deps.pluginMCP != nil {
|
|
rawMCP = mergeMCPServers(rawMCP, deps.pluginMCP())
|
|
}
|
|
if deps.mcp != nil {
|
|
if mgr := deps.mcp(rawMCP, strings.TrimSpace(effNow.GetString("tools.mcp.overrides", ""))); mgr != nil {
|
|
cfg.MCP = mgr
|
|
}
|
|
}
|
|
// Native squad tools do not yet expose a fail-closed result hook to the
|
|
// project observation outbox. Project runs therefore use the audited
|
|
// runCtx retrieval/tool path only; enabling these direct providers would
|
|
// let their results reach the next model turn without source analysis.
|
|
if rc.Run().ProjectID != "" {
|
|
cfg.Search = nil
|
|
cfg.Exec = nil
|
|
cfg.Fetch = nil
|
|
cfg.MCP = nil
|
|
}
|
|
cfg.CodeOrchestration = rc.Run().ProjectID == "" && effNow.GetBool("runtime.codeOrchestration", false)
|
|
if ask, ok := rc.(interface {
|
|
AskUser(card core.HITLCard) (core.HITLResponse, error)
|
|
}); ok {
|
|
cfg.Ask = func(_ context.Context, question string) (string, error) {
|
|
resp, err := ask.AskUser(core.HITLCard{
|
|
Question: question, Kind: core.HITLKindInput, AllowFreeInput: true,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if strings.TrimSpace(resp.FreeText) != "" {
|
|
return strings.TrimSpace(resp.FreeText), nil
|
|
}
|
|
return resp.ChoiceID, nil
|
|
}
|
|
}
|
|
cfg.Trace = deps.trace
|
|
// N1 WP4:小队成员的内核 ask 决策经 GateOptions.PermissionAsk 到达
|
|
// runCtx 的 HITL 确认卡(宿主无实现则 nil,内核保持 ask=拒绝旧行为)。
|
|
cfg.PermissionAsk = runtime.PermissionAskOf(rc)
|
|
if host, ok := rc.(interface {
|
|
OpenCheckpoints() (*checkpoint.Log, error)
|
|
CheckpointAnchor() string
|
|
}); ok {
|
|
if lg, err := host.OpenCheckpoints(); err == nil {
|
|
cfg.Checkpoints = lg
|
|
anchor := host.CheckpointAnchor()
|
|
cfg.CheckpointAnchor = func() string { return anchor }
|
|
}
|
|
}
|
|
return cfg, true
|
|
}
|
|
}
|