Files
Agent-System-Backend/internal/runtime/model.go
T
weichen f965c5877f Enforce settings ownership and versioned shared configuration
Why: Personal project rows and credentials could override system policy and split shared business configuration by actor.

Evidence: SELF receipts /tmp/project-settings-backend-full-20261014-973d52c1.json and /tmp/rdm-settings-backend-final-green-20261014-03.json preserve full commands, object snapshots and output. PostgreSQL and current-head CI remain NOT_RUN.

Impact: Add scope-aware APIs, a fail-closed optional project configuration authorizer, CAS updates and explicit atomic migration. Preserve legacy rows and private preferences; protected secrets and runtime model routes obey system ownership.

Refs: P1/P2/P8, docs/records/2026-10-14-rdm-settings-completion-contract.md
2026-10-14 02:28:41 +00:00

104 lines
3.1 KiB
Go

package runtime
import (
"strings"
core "agent-system-core"
)
// SecretLookup is wired by the composition root. Protected services resolve only
// system authority; ok=true with an empty value means explicit revocation.
var SecretLookup func(owner, service string) (string, bool)
// LookupSecret 给组合根以外的调用方使用。owner 为空时用 local。
func LookupSecret(owner, service, fallback string) string {
return overlayKey(owner, service, fallback)
}
func overlayKey(owner, service, fallback string) string {
if SecretLookup == nil {
return fallback
}
if owner == "" {
owner = "local"
}
if v, ok := SecretLookup(owner, service); ok {
return v
}
return fallback
}
// Real Service.Effective snapshots carry the trusted managed-model policy and
// ignore legacy profile routes. Unmanaged standalone runtime snapshots retain
// their own profile semantics. Lightweight model/endpoint falls back to chat.
// ContextInjection 是一条条件注入,由 runmgr 从存储读出,组装时再转换成 ctxasm 的形状。
type ContextInjection struct {
Keys []string
Secondary string
Logic string
Content string
Constant bool
ScanDepth int
BudgetChars int
}
// ResolvedModel 是一次路由解析的结果。
type ResolvedModel struct {
Kind string // chat | lightweight
Model string
Base string
Key string
}
// ResolveChatModel resolves system-managed chat settings in actual runs.
func ResolveChatModel(prof core.AgentProfile, eff core.EffectiveSettings) ResolvedModel {
m := eff.GetString("models.chat.model", "")
if !eff.GetBool("_policy.managedModels", false) && prof.Model.Chat != "" {
m = prof.Model.Chat
}
return ResolvedModel{Kind: "chat", Model: m,
Base: eff.GetString("models.chat.baseUrl", ""),
Key: overlayKey(prof.Owner, "models.chat", eff.GetString("models.chat.apiKey", ""))}
}
// ChatFallbacks 解析 models.chat.fallbacks。默认空,不改变现有模型选择。
func ChatFallbacks(eff core.EffectiveSettings) []string {
raw := strings.TrimSpace(eff.GetString("models.chat.fallbacks", ""))
if raw == "" {
return nil
}
var out []string
seen := map[string]bool{}
for _, part := range strings.Split(raw, ",") {
name := strings.TrimSpace(part)
if name == "" || seen[name] {
continue
}
seen[name] = true
out = append(out, name)
}
return out
}
// ResolveLightweightModel resolves the managed lightweight route, then chat.
func ResolveLightweightModel(prof core.AgentProfile, eff core.EffectiveSettings) ResolvedModel {
m := eff.GetString("models.lightweight.model", "")
base := eff.GetString("models.lightweight.baseUrl", "")
key := overlayKey(prof.Owner, "models.lightweight", eff.GetString("models.lightweight.apiKey", ""))
if !eff.GetBool("_policy.managedModels", false) && prof.Model.Lightweight != "" {
m = prof.Model.Lightweight
}
if m == "" || base == "" {
// 回落主档(模型与端点分别回落,允许轻量仅覆盖模型名)。
chat := ResolveChatModel(prof, eff)
if m == "" {
m = chat.Model
}
if base == "" {
base, key = chat.Base, chat.Key
}
}
return ResolvedModel{Kind: "lightweight", Model: m, Base: base, Key: key}
}