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
104 lines
3.1 KiB
Go
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}
|
|
}
|