Why: Validate W6 against account-audit migration v31 on the current authoritative base. Evidence: Focused store/runmgr/migrator and full Go tests passed in the isolated probe after resolving migration collisions. Impact: Renumbers unpublished W6 migrations to v32-v36; this probe commit is not a shipping branch or live migration. Refs: feature/rdm-two-trees-latest-templates W6 integration probe
1080 lines
43 KiB
Go
1080 lines
43 KiB
Go
// Package app 组合根:装配存储(嵌入式/PG 档)、总线、注册表、服务、runmgr、
|
||
// 认证、插件宿主与 HTTP 服务。
|
||
package app
|
||
|
||
import (
|
||
"context"
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"net"
|
||
"net/http"
|
||
"os"
|
||
"os/signal"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"syscall"
|
||
"time"
|
||
|
||
"error-code-system/errcode"
|
||
"logging-system/logsys"
|
||
"logging-system/sink"
|
||
|
||
"agent-native/runtime/checkpoint"
|
||
agentllm "agent-native/runtime/llm"
|
||
agentmcp "agent-native/runtime/mcp"
|
||
"agent-native/runtime/policy"
|
||
"agent-native/runtime/telemetry"
|
||
agenttools "agent-native/runtime/tools"
|
||
nativeadp "agent-squad/adapters/native"
|
||
"agent-system-backend/internal/blobstore"
|
||
"agent-system-backend/internal/bus"
|
||
"agent-system-backend/internal/cardsvc"
|
||
"agent-system-backend/internal/cmdbus"
|
||
"agent-system-backend/internal/config"
|
||
"agent-system-backend/internal/genui"
|
||
"agent-system-backend/internal/hostapi"
|
||
"agent-system-backend/internal/intentgate"
|
||
"agent-system-backend/internal/intentsem"
|
||
"agent-system-backend/internal/pgbus"
|
||
"agent-system-backend/internal/plugins"
|
||
"agent-system-backend/internal/profiles"
|
||
"agent-system-backend/internal/prompts"
|
||
"agent-system-backend/internal/rag"
|
||
"agent-system-backend/internal/ratelimit"
|
||
"agent-system-backend/internal/runmgr"
|
||
"agent-system-backend/internal/runtime"
|
||
"agent-system-backend/internal/server"
|
||
"agent-system-backend/internal/settings"
|
||
"agent-system-backend/internal/skills"
|
||
"agent-system-backend/internal/squadadapter"
|
||
"agent-system-backend/internal/store"
|
||
"agent-system-backend/internal/surfacesvc"
|
||
"agent-system-backend/internal/tools"
|
||
"agent-system-backend/internal/workflow"
|
||
"agent-system-backend/pkg/extension"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
type Options struct {
|
||
Addr string
|
||
DataDir string
|
||
PluginsDir string
|
||
ForceMock bool
|
||
StorageDSN string // 空 = 嵌入式 SQLite 档;非空 = PostgreSQL 档(多用户/多实例)
|
||
ArtifactsDir string // PG 档工件对象目录(空 = <DataDir>/artifacts)
|
||
AuthMode string // auto(默认:PG 档开、嵌入式关)| on | off
|
||
RequireProjectContext bool
|
||
InstanceID string // 空 = 主机名:监听地址;写入 runs.instance_id,启动对账只收口本实例
|
||
InsecureListen bool
|
||
AllowedHosts []string
|
||
// DisableUploadIndexing EXT-9:true 时上传端点不自动写检索索引(发布决定
|
||
// 移交下游命令层);零值=false=现状。由 link 层自 extension.Options 透传。
|
||
DisableUploadIndexing bool
|
||
// DisableConvCardMirror EXT-12:true 时会话卡不镜像进项目公共流(不发布
|
||
// 即不持久,重放不含会话卡);零值=false=现状全量镜像。
|
||
DisableConvCardMirror bool
|
||
// DisableExecTools 启动级硬关闭执行原语(run_shell / run_python /
|
||
// register_artifact 与小队内核执行配置):用户设置 runtime.exec 无法覆盖;
|
||
// 零值=false=现状。
|
||
DisableExecTools bool
|
||
// ExecBackend 执行后端(EXT-16):空/direct=现状直启;sandbox=bwrap 隔离档。
|
||
// 硬关闭 DisableExecTools 优先级高于后端选择。
|
||
ExecBackend string
|
||
// ExecDepsDir sandbox 档只读依赖层(site-packages 等,空=不挂):修复
|
||
// --tmpfs /home 遮蔽 ~/.local 导致交付物生成链路不可用(F3)。
|
||
ExecDepsDir string
|
||
// ExecWorkspace = "overlay" 时 sandbox 档启用 S0 工作区(冻结基底+私有
|
||
// upper+CAS 回收);空 = EXT-16 现状直写会话目录。
|
||
ExecWorkspace string
|
||
// Tier 部署档(协作存储 KTD1):空=auto(DSN→PG 否则 SQLite);local=SQLite;
|
||
// team=PG fail-closed(缺 DSN/未认证拒绝启动;PG 故障拒绝就绪,不回落 SQLite)。
|
||
Tier string
|
||
// PGPool PG 连接预算(KTD12;零值走默认)。仅 PG 档生效。
|
||
PGPool store.PoolOptions
|
||
}
|
||
|
||
// instanceTag 稳定执行身份:tier=team 时强制 host:addr(多实例互异),
|
||
// 本地档退化为 local(单实例语义)。
|
||
func instanceTag(opt Options) string {
|
||
if opt.Tier == "team" || opt.StorageDSN != "" {
|
||
return hostTag() + ":" + opt.Addr
|
||
}
|
||
return "local"
|
||
}
|
||
|
||
func hostTag() string {
|
||
h, err := os.Hostname()
|
||
if err != nil || h == "" {
|
||
return "host"
|
||
}
|
||
return h
|
||
}
|
||
|
||
// tierName 输出部署档名(诊断统一拼写:空 = auto)。
|
||
func tierName(opt Options) string {
|
||
if opt.Tier == "" {
|
||
return "auto"
|
||
}
|
||
return opt.Tier
|
||
}
|
||
|
||
// TierRefusal 部署档门禁(KTD1):返回阻止启动的问题(空 = 放行)。
|
||
// team 档 fail-closed:缺 DSN、未开认证拒绝;PG 故障的拒绝发生在其后的
|
||
// OpenPostgres(Run 对 team 档的错误显式声明不回落 SQLite)。
|
||
func TierRefusal(opt Options) []string {
|
||
return config.Resolved{Config: config.Config{
|
||
Addr: opt.Addr, AuthMode: opt.AuthMode, StorageDSN: opt.StorageDSN, Tier: opt.Tier,
|
||
PGMaxOpen: opt.PGPool.MaxOpenConn, PGMaxIdle: opt.PGPool.MaxIdleConn,
|
||
PGConnMaxIdleSec: opt.PGPool.ConnMaxIdleTimeSec, PGConnMaxLifetimeSec: opt.PGPool.ConnMaxLifetimeSec,
|
||
PGQueryTimeoutSec: opt.PGPool.QueryTimeoutSec,
|
||
}}.TierProblems()
|
||
}
|
||
|
||
// latestVersion 返回已应用迁移的最高版本(0 = 空账本)。
|
||
func latestVersion(versions []int) int {
|
||
max := 0
|
||
for _, v := range versions {
|
||
if v > max {
|
||
max = v
|
||
}
|
||
}
|
||
return max
|
||
}
|
||
|
||
// Run 装配并阻塞服务直至 SIGINT/SIGTERM(优雅停机:中断在途 run、冲净事件缓冲)。
|
||
func Run(opt Options) error {
|
||
logger := logsys.New(logsys.Options{
|
||
Service: "agent-system", Module: "app", Level: logsys.LevelInfo,
|
||
Sinks: []logsys.Sink{&sink.Console{Out: os.Stderr, Format: "json"}},
|
||
})
|
||
logger.Info("agent-system starting", "addr", opt.Addr, "data", opt.DataDir, "tier", tierName(opt))
|
||
resolved := config.Resolved{Config: config.Config{
|
||
Addr: opt.Addr, AuthMode: opt.AuthMode, StorageDSN: opt.StorageDSN, InsecureListen: opt.InsecureListen,
|
||
Tier: opt.Tier, PGMaxOpen: opt.PGPool.MaxOpenConn, PGMaxIdle: opt.PGPool.MaxIdleConn,
|
||
PGConnMaxIdleSec: opt.PGPool.ConnMaxIdleTimeSec, PGConnMaxLifetimeSec: opt.PGPool.ConnMaxLifetimeSec,
|
||
PGQueryTimeoutSec: opt.PGPool.QueryTimeoutSec,
|
||
}}
|
||
if msg := resolved.ListenRefusal(); msg != "" {
|
||
return fmt.Errorf("listen guard: %s", msg)
|
||
}
|
||
// 部署档门禁(KTD1 fail-closed):team 档缺 DSN/未开认证直接拒绝启动;
|
||
// local 档带 DSN 是配置错误。此处与 config.TierProblems 同规则(纵深防御,
|
||
// 服务入口不依赖 CLI 是否校验)。
|
||
if msgs := TierRefusal(opt); len(msgs) > 0 {
|
||
return fmt.Errorf("tier guard: %s", strings.Join(msgs, ";"))
|
||
}
|
||
if opt.Tier == "team" && !opt.DisableExecTools && opt.ExecBackend == "" {
|
||
log.Printf("warning: team 档未配置执行隔离(disableExecTools 或 execBackend);向真实多用户开放前必须补齐(方案 KTD1)")
|
||
}
|
||
if opt.InsecureListen && !config.IsLoopbackListen(opt.Addr) && !resolved.AuthEnabled() {
|
||
log.Printf("warning: listening on %s without authentication because insecureListen is set", opt.Addr)
|
||
}
|
||
var st *store.Store
|
||
var err error
|
||
if opt.StorageDSN != "" {
|
||
artifacts := opt.ArtifactsDir
|
||
if artifacts == "" {
|
||
artifacts = filepath.Join(opt.DataDir, "artifacts")
|
||
}
|
||
// 共享工件(U5/KTD9):配置了 S3 兼容对象存储时注入(多实例共享);
|
||
// 未配置回落本地目录(单实例/共享卷)。凭据只经环境(不入库不入报告)。
|
||
blobs, berr := blobstore.NewS3FromEnv()
|
||
if berr != nil {
|
||
return fmt.Errorf("blobstore s3: %w", berr)
|
||
}
|
||
if blobs != nil {
|
||
log.Printf("artifacts: shared via S3-compatible object storage (prefix %q)", blobs.Prefix)
|
||
}
|
||
st, err = store.OpenPostgresWithOptions(opt.StorageDSN, artifacts, opt.PGPool, blobs)
|
||
if err != nil && opt.Tier == "team" {
|
||
// team 档拒绝就绪(R2):PG 故障不回落 SQLite,错误必须明说。
|
||
return fmt.Errorf("team 档打开 PostgreSQL 失败,拒绝就绪(不回落 SQLite): %w", err)
|
||
}
|
||
} else {
|
||
st, err = store.OpenSQLite(filepath.Join(opt.DataDir, "agent-system.db"))
|
||
}
|
||
if err != nil {
|
||
return fmt.Errorf("open store: %w", err)
|
||
}
|
||
defer st.Close()
|
||
// U1 I3:svc 注入的服务实例身份(manifest.instance_id)与库内 marker 对齐;
|
||
// 未设置则跳过(兼容未接线旧调用方)。与 --instance(runs 进程标识)无关。
|
||
if sysID := strings.TrimSpace(os.Getenv("AGENT_SYSTEM_INSTANCE_ID")); sysID != "" {
|
||
dbref := strings.TrimSpace(os.Getenv("AGENT_SYSTEM_DBREF"))
|
||
if dbref == "" {
|
||
if opt.StorageDSN != "" {
|
||
dbref = "pg"
|
||
} else {
|
||
dbref = "sqlite"
|
||
}
|
||
}
|
||
if err := st.VerifySystemIdentity(sysID, dbref); err != nil {
|
||
return fmt.Errorf("system identity: %w", err)
|
||
}
|
||
}
|
||
pool := st.Pool()
|
||
if versions, verr := st.MigrationStatus(); verr == nil {
|
||
log.Printf("storage tier: %s (dialect=%s pool=%d/%d idleSec=%d lifetimeSec=%d schema=v%d)",
|
||
tierName(opt), st.Dialect(), pool.MaxOpenConn, pool.MaxIdleConn, pool.ConnMaxIdleTimeSec,
|
||
pool.ConnMaxLifetimeSec, latestVersion(versions))
|
||
} else {
|
||
log.Printf("storage tier: %s (dialect=%s pool=%d/%d schema=? migration status unavailable: %v)",
|
||
tierName(opt), st.Dialect(), pool.MaxOpenConn, pool.MaxIdleConn, verr)
|
||
}
|
||
|
||
b := bus.New()
|
||
reg := tools.NewRegistry()
|
||
if err := reg.RegisterBuiltin(); err != nil {
|
||
return fmt.Errorf("builtin tools: %w", err)
|
||
}
|
||
// fs 预留能力:工具族实现但 Hidden(runtime.fsTools 未开时模型与 /api/tools 均不可见)。
|
||
if err := tools.RegisterFSTools(reg); err != nil {
|
||
return fmt.Errorf("fs tools: %w", err)
|
||
}
|
||
// 执行原语(agent-native 交付):shell/python 在会话工作区执行 + 产物登记桥,
|
||
// runtime.exec=false 可整体禁用(fail-closed)。生成文档/表格/演示稿的方法
|
||
// 在提示词库 base.deliverables,不在系统工具里。启动参数 DisableExecTools
|
||
// 为不受用户设置影响的硬关闭(多用户部署的默认安全档)。
|
||
tools.SetExecDisabled(opt.DisableExecTools)
|
||
if opt.DisableExecTools {
|
||
log.Printf("exec tools: disabled by startup option (runtime.exec settings are ignored)")
|
||
}
|
||
// EXT-16:执行后端选择(direct|sandbox);未知名字启动即失败(fail-closed)。
|
||
if err := tools.SetExecBackend(opt.ExecBackend); err != nil {
|
||
return fmt.Errorf("exec backend: %w", err)
|
||
}
|
||
if tools.ExecBackendName() == "sandbox" {
|
||
log.Printf("exec tools: backend=sandbox (bwrap: no network, ro root, session-only writes, proc limit)")
|
||
}
|
||
if opt.ExecDepsDir != "" {
|
||
tools.SetExecDepsDir(opt.ExecDepsDir)
|
||
log.Printf("exec tools: sandbox deps layer=%s", opt.ExecDepsDir)
|
||
}
|
||
if opt.ExecWorkspace == "overlay" {
|
||
tools.SetWorkspaceOverlay(true)
|
||
if tools.ExecBackendName() == "sandbox" {
|
||
log.Printf("exec tools: workspace=overlay (frozen base + private upper + CAS apply)")
|
||
} else {
|
||
log.Printf("exec tools: workspace=overlay ignored (backend is not sandbox)")
|
||
}
|
||
} else if opt.ExecWorkspace != "" {
|
||
return fmt.Errorf("exec workspace: unknown mode %q (overlay)", opt.ExecWorkspace)
|
||
}
|
||
if err := tools.RegisterExecTools(reg); err != nil {
|
||
return fmt.Errorf("exec tools: %w", err)
|
||
}
|
||
if err := tools.RegisterInterpreterTools(reg); err != nil {
|
||
return fmt.Errorf("interpreter tools: %w", err)
|
||
}
|
||
if err := tools.RegisterCodeTool(reg); err != nil {
|
||
return fmt.Errorf("code orchestration tool: %w", err)
|
||
}
|
||
setSvc := settings.NewService(st)
|
||
effBoot := setSvc.Effective(store.OwnerDefault, "")
|
||
traceFn := telemetry.StartFunc(logger,
|
||
telemetry.WithServiceName(effBoot.GetString("telemetry.serviceName", "agent-system-backend")),
|
||
telemetry.WithEndpoint(effBoot.GetString("telemetry.otlpEndpoint", "")))
|
||
setSvc.SetSystemOverride(map[string]any{"storage": map[string]any{"dataDir": opt.DataDir}})
|
||
// 点路径键展开必须在读 tools.tavily.apiKeys 之前,否则启动仍注册旧池。
|
||
norm, err := setSvc.NormalizeStored()
|
||
if err != nil {
|
||
return fmt.Errorf("normalize settings: %w", err)
|
||
}
|
||
for _, p := range norm.MaskPaths {
|
||
log.Printf("settings: warning: stored value is the mask placeholder at %s", p)
|
||
}
|
||
if norm.DottedKeys > 0 {
|
||
log.Printf("settings: normalized %d dotted key(s) in %d scope(s)", norm.DottedKeys, norm.Scopes)
|
||
}
|
||
if err := setSvc.SnapshotAndMigrateSecrets(); err != nil {
|
||
return fmt.Errorf("migrate secrets: %w", err)
|
||
}
|
||
runtime.SecretLookup = func(owner, service string) (string, bool) {
|
||
if owner == "" {
|
||
owner = store.OwnerDefault
|
||
}
|
||
if v, ok := st.ActiveSecret(owner, service); ok {
|
||
return v, true
|
||
}
|
||
// P2 受管:密钥统一迁入 secrets 表后,系统层受管密钥挂在系统行
|
||
//(owner=settings.OwnerSystemRow);租户未单独配置时回退系统密钥。
|
||
return st.ActiveSecret(settings.OwnerSystemRow, service)
|
||
}
|
||
rag.SecretLookup = func(service, fallback string) string {
|
||
return runtime.LookupSecret(store.OwnerDefault, service, fallback)
|
||
}
|
||
// 网络搜索(Tavily):始终注册,密钥按调用时读取(设置页/svc 播种改了
|
||
// 即刻生效——此前启动时快照密钥,播种后必须重启工具才注册)。
|
||
// 未配置时调用返回明确的「未配置」提示,不静默降级为空结果。
|
||
if err := tools.RegisterLazySearchTools(reg, func() agenttools.SearchProvider {
|
||
effNow := setSvc.Effective(store.OwnerDefault, "")
|
||
tavilyNow := effNow.GetString("tools.tavily.apiKeys", "")
|
||
if active, ok := st.ActiveSecret(store.OwnerDefault, "tools.tavily"); ok {
|
||
tavilyNow = active
|
||
}
|
||
keys := splitKeyPool(tavilyNow)
|
||
if len(keys) == 0 {
|
||
return nil
|
||
}
|
||
return agenttools.NewTavilyProvider(effNow.GetString("tools.tavily.endpoint", ""), keys)
|
||
}); err != nil {
|
||
return fmt.Errorf("search tools: %w", err)
|
||
}
|
||
promptLib, err := prompts.DefaultLibrary()
|
||
if err != nil {
|
||
return fmt.Errorf("prompt library: %w", err)
|
||
}
|
||
promptSvc, err := prompts.NewService(st, promptLib)
|
||
if err != nil {
|
||
return fmt.Errorf("prompt service: %w", err)
|
||
}
|
||
// 小队角色指令接提示词库(agent.squad.<kind>;未命中回落内联文案)。
|
||
squadadapter.RoleInstructions = func(kind string) (string, bool) {
|
||
body, err := promptSvc.AgentPrompt(store.OwnerDefault, "agent.squad."+kind)
|
||
return body, err == nil
|
||
}
|
||
// 技能种子:交付配方与 house-style 落 <data>/skills/。
|
||
// 未改过的内置文件随版本升级;用户改过的保留并记 drift。
|
||
skillsRoot := filepath.Join(opt.DataDir, "skills")
|
||
if res, serr := skills.Seed(skillsRoot); serr != nil {
|
||
log.Printf("skills: seed builtin recipes failed: %v", serr)
|
||
} else if res.Written > 0 || res.Upgraded > 0 || len(res.Drifted) > 0 {
|
||
log.Printf("skills: wrote %d, upgraded %d into %s", res.Written, res.Upgraded, skillsRoot)
|
||
for _, path := range res.Drifted {
|
||
log.Printf("skills: kept user revision (drift) %s", path)
|
||
}
|
||
}
|
||
tools.SetSkillsRoot(skillsRoot)
|
||
// 小队的 agent-native 内核工厂(组合根装配,逐次运行时从 settings 解析):
|
||
// 模型走 settings.models.chat 同一路由;搜索/解释器/MCP/技能根复用产品配置;
|
||
// 委派桥接产品 SpawnSubagent。主模型未配置时返回未启用(不阻塞产品)。
|
||
var pluginHost *plugins.Host
|
||
squadadapter.AgentNativeOptions = func(rc squadadapter.Ctx) (nativeadp.Config, bool) {
|
||
effNow := setSvc.Effective(store.OwnerDefault, "")
|
||
route := runtime.ResolveChatModel(core.AgentProfile{}, 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: &agentllm.Client{BaseURL: route.Base, APIKey: route.Key, HTTPClient: &http.Client{}},
|
||
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)
|
||
tavilyNow := effNow.GetString("tools.tavily.apiKeys", "")
|
||
if active, ok := st.ActiveSecret(store.OwnerDefault, "tools.tavily"); ok {
|
||
tavilyNow = active
|
||
}
|
||
if keys := splitKeyPool(tavilyNow); 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(skillsRoot); err == nil && st2.IsDir() {
|
||
cfg.SkillsRoots = []string{skillsRoot}
|
||
}
|
||
rawMCP := strings.TrimSpace(effNow.GetString("tools.mcp.servers", ""))
|
||
if pluginHost != nil {
|
||
rawMCP = mergeMCPServers(rawMCP, pluginHost.MCPServers())
|
||
}
|
||
if mgr := mcpManagerFor(rawMCP, strings.TrimSpace(effNow.GetString("tools.mcp.overrides", ""))); mgr != nil {
|
||
cfg.MCP = mgr
|
||
}
|
||
cfg.CodeOrchestration = 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 = traceFn
|
||
// 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
|
||
}
|
||
profSvc := profiles.New(st)
|
||
if err := profSvc.EnsureDefault(); err != nil {
|
||
return fmt.Errorf("default profile: %w", err)
|
||
}
|
||
if opt.ForceMock {
|
||
// runtime.* 自 #188 起为系统受管项:启动选项属受信操作员输入,
|
||
// 播种走系统层(对所有租户生效);用户层写入会被受管拦截拒绝,
|
||
// 导致 serve --mock 直接启动失败(multi-instance 集成回归)。
|
||
if err := setSvc.MergeSystem(map[string]any{"runtime": map[string]any{"mock": true}}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
convRunner := &runtime.LLMRunner{Prompts: promptSvc, SkillsDir: skillsRoot, Trace: traceFn}
|
||
subRunner := &runtime.SubLLMRunner{Prompts: promptSvc, Trace: traceFn}
|
||
mockRunner := &runtime.MockRunner{}
|
||
mgr := runmgr.New(st, b, reg, profSvc, setSvc, convRunner, subRunner, mockRunner, &runtime.MockSubagent{})
|
||
instanceID := opt.InstanceID
|
||
if instanceID == "" {
|
||
host, err := os.Hostname()
|
||
if err != nil || host == "" {
|
||
host = "local"
|
||
}
|
||
instanceID = host + ":" + opt.Addr
|
||
}
|
||
st.SetInstanceID(instanceID)
|
||
mgr.SetInstanceID(instanceID)
|
||
if err := mgr.ReconcileInterrupted(); err != nil {
|
||
return fmt.Errorf("reconcile interrupted runs: %w", err)
|
||
}
|
||
// 后台子代理通知重启重投(N2 WP5):未投递结果装回队列并按空闲语义投递。
|
||
mgr.LoadPendingNotices()
|
||
mgr.SetSquadRunner(&runtime.SquadRunner{})
|
||
|
||
// 意图闸门:插件意图 overlay → agent-intent 注册表(未装载意图 = 行为回退)。
|
||
gate := intentgate.New()
|
||
mgr.SetGate(gate)
|
||
|
||
pluginHost = plugins.New(st, reg, setSvc, opt.PluginsDir)
|
||
pluginHost.SetSkillsRoot(skillsRoot)
|
||
hostTok := make([]byte, 16)
|
||
_, _ = rand.Read(hostTok)
|
||
hostSrv := &hostapi.Server{Token: hex.EncodeToString(hostTok), Store: st}
|
||
hostLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||
if err != nil {
|
||
return fmt.Errorf("host api: %w", err)
|
||
}
|
||
defer hostLn.Close()
|
||
go func() { _ = http.Serve(hostLn, hostSrv.Handler()) }()
|
||
pluginHost.SetHostAPI("http://"+hostLn.Addr().String(), hostSrv.Token)
|
||
pluginHost.OnIntents = func(overlay []json.RawMessage) error { return gate.Rebuild(context.Background(), overlay) }
|
||
wfReg := workflow.NewRegistry()
|
||
pluginHost.OnWorkflows = func(defs []core.WorkflowDef, scripts map[string]string) error {
|
||
wfReg.Set(defs, scripts)
|
||
return nil
|
||
}
|
||
for id, perr := range pluginHost.LoadDir() {
|
||
if perr != nil {
|
||
log.Printf("[plugins] %s: %s: %v", id, errcode.SysUnavailable, perr) // fail-closed:单个失败不阻断启动
|
||
logger.Error("plugin load failed", perr, "plugin", id, "code", string(errcode.SysUnavailable))
|
||
}
|
||
}
|
||
// P2 受管:插件声明里带 scope=system 的设置项同样纳入受管路径
|
||
//(user/project 写入拒绝、生效链剥除)。
|
||
setSvc.SetExtraSchema(pluginHost.SettingsSchema())
|
||
|
||
// 检索管线(4.12):分块+全文+(配置时)向量混合与 rerank;未配置自动退化为全文。
|
||
pipe := rag.NewPipeline(st, setSvc, nil, nil)
|
||
embedder := rag.SwitchingEmbedder{HTTP: rag.NewHTTPEmbedder(pipe.EmbeddingRoute), Provider: func() string {
|
||
return setSvc.Effective(store.OwnerDefault, "").GetString("models.embedding.provider", "")
|
||
}}
|
||
pipe.SetEmbedder(embedder)
|
||
pipe.SetReranker(rag.NewHTTPReranker(pipe.RerankRoute))
|
||
mgr.SetRetriever(pipe)
|
||
mgr.SetIndexer(pipe)
|
||
|
||
// 意图慢通道(I2):语义向量裁决 + 轻量模型命令生成。
|
||
// 两个开关都默认关——此时识别行为与 I1 完全一致(fail-closed 分层);
|
||
// 开启语义需已配置 embedding 端点,开启生成需可用的轻量/主模型档。
|
||
if setSvc.Effective(store.OwnerDefault, "").GetBool("runtime.intentSemantic", false) {
|
||
gate.SetSemantic(intentsem.EmbeddingScorer{Provider: embedder}, 0)
|
||
}
|
||
if setSvc.Effective(store.OwnerDefault, "").GetBool("runtime.intentGenerator", false) {
|
||
effNow := setSvc.Effective(store.OwnerDefault, "")
|
||
light := runtime.ResolveLightweightModel(core.AgentProfile{}, effNow)
|
||
client := &agentllm.Client{BaseURL: light.Base, APIKey: light.Key, HTTPClient: &http.Client{}}
|
||
gate.SetGenerator(intentsem.LightGenerator{Stream: client.Stream, Model: light.Model})
|
||
}
|
||
// fs 预留能力沙箱根:<DataDir>/workspaces/<owner>/<conversationID>/。
|
||
mgr.SetWorkspaceRoot(filepath.Join(opt.DataDir, "workspaces"))
|
||
// 中断子代理续跑(N2 WP5):依赖转录目录(workspaceRoot)与派生请求。
|
||
mgr.ResumeInterruptedChildren()
|
||
|
||
// 收件箱任务卡:事件走会话事件管道(持久化/seq/重放);关闭即 T0 基线。
|
||
cardSvc := cardsvc.New(st, mgr.EmitCardEvent)
|
||
cardSvc.SetProjectSharedEmitter(mgr.EmitProjectSharedCardEvent)
|
||
surfaceSvc := surfacesvc.New(st, mgr.EmitCardEvent)
|
||
go func() { // 启动恢复:重发「快照已落盘、事件未发出」的待恢复投影(KTD)。
|
||
if n, err := surfaceSvc.RecoverPendingEvents(context.Background()); err == nil && n > 0 {
|
||
log.Printf("surface: recovered %d pending events", n)
|
||
}
|
||
}()
|
||
mgr.SetCards(cardSvc)
|
||
// 执行身份与命令消费(协作存储 U5):PG 档启动跨实例命令轮询;
|
||
// SQLite 本地档无跨实例命令(StartCommandConsumer 自行短路)。
|
||
mgr.SetInstanceTag(instanceTag(opt), opt.Addr)
|
||
stopCommands := mgr.StartCommandConsumer(context.Background())
|
||
defer stopCommands()
|
||
// 执行预算(协作存储 U3,R8):runtime.execBudgetGlobal / runtime.execBudgetPerUser
|
||
// 全零=不限(本地档默认,行为不变);开启后执行许可共享账本生效,
|
||
// 待执行队列上限 runtime.execQueueMax(默认 200,容量合同数值待 U7 实测调整)。
|
||
mgr.SetExecutionBudgetFn(func() store.ExecutionBudget {
|
||
eff := setSvc.Effective(store.OwnerDefault, "")
|
||
return store.ExecutionBudget{
|
||
Global: int(eff.GetNumber("runtime.execBudgetGlobal", 0)),
|
||
PerUser: int(eff.GetNumber("runtime.execBudgetPerUser", 0)),
|
||
}
|
||
}, int(setSvc.Effective(store.OwnerDefault, "").GetNumber("runtime.execQueueMax", 200)))
|
||
|
||
// 命令总线(提案 T3/D27/D30):四入口同源分发 + 审计 + 危险内联确认卡。
|
||
bus := cmdbus.New(st, cardSvc, func() time.Time { return time.Now().UTC() })
|
||
// U5:页面动作分发口——surfacesvc.SubmitAction 经命令总线四入口同源分发
|
||
//(审计信封、参数校验、DangerHigh 内联确认卡全复用;Confirmed 不透传)。
|
||
surfaceSvc.SetDispatcher(func(ctx context.Context, d surfacesvc.ActionDispatch) (surfacesvc.ActionOutcome, error) {
|
||
resp := bus.Dispatch(ctx, cmdbus.Request{ID: d.CommandID, Params: d.Params,
|
||
Entrance: "button", Owner: d.Owner, ConversationID: d.ConversationID})
|
||
state := "committed"
|
||
if resp.Receipt.State != "" {
|
||
state = string(resp.Receipt.State)
|
||
}
|
||
cardID := ""
|
||
if resp.Card != nil {
|
||
cardID = string(resp.Card.ID)
|
||
}
|
||
return surfacesvc.ActionOutcome{State: state, CardID: cardID, Receipt: resp.Receipt, Payload: resp.Payload}, nil
|
||
})
|
||
// U3:generative-ui 工具注入闸——activation 台账关闭时命令不投影给该 owner
|
||
// 的模型(禁用即不注入能力;执行端守卫仍在 surfacesvc)。
|
||
bus.SetToolGate(func(owner, commandID string) bool {
|
||
if !strings.HasPrefix(commandID, genui.PluginID+".") {
|
||
return true
|
||
}
|
||
row, err := surfaceSvc.Activation(context.Background(), owner, genui.PluginID)
|
||
return err == nil && row.Enabled
|
||
})
|
||
// 自动建议页面 opt-in:settings ∧ activation;预算/冷却在 genui.Auto。
|
||
mgr.SetGenUIAuto(genui.NewAuto(
|
||
func(owner string) bool {
|
||
row, err := surfaceSvc.Activation(context.Background(), owner, genui.PluginID)
|
||
return err == nil && row.Enabled
|
||
},
|
||
func(owner string) bool {
|
||
return setSvc.Effective(owner, "").GetBool(genui.SettingAutoPresent, false)
|
||
},
|
||
func(owner, kind, detail string) {
|
||
_ = st.AppendAuditEvent(store.AuditEvent{Owner: owner, Action: kind, Evidence: detail})
|
||
},
|
||
))
|
||
|
||
bus.SetPendingStore(st)
|
||
pluginHost.OnCommands = bus.RegisterPluginCommands
|
||
// hostcaps:hostapi 闭包与进程内插件句柄共用的 owner 作用域能力(EXT-1 批注 1)。
|
||
authzSlot := &projectAuthz{}
|
||
projectSlot := &projectLifecycleSlot{}
|
||
contextSlot := &projectContextSlot{}
|
||
caps := hostcaps{projects: projectSlot, contexts: contextSlot, st: st, pipe: pipe, set: setSvc, authz: authzSlot, surfaces: surfaceSvc}
|
||
// EXT-13:回合内检索(knowledge_search / @knowledge 注入)与 HTTP/Host 检索面
|
||
// 同一适配器授权;槽为共享指针,扩展在 applyCompiled 期间注册后即生效。
|
||
mgr.SetSharedKnowledgeAllow(func(owner string, projectID core.ID) bool {
|
||
ok, err := authzSlot.Authorize(owner, projectID)
|
||
return err == nil && ok
|
||
})
|
||
// EXT-14:命令即工具——带 exposeAsTool 的命令投影进回合工具清单,模型调用
|
||
// 经命令总分发(Entrance=agent,同审计信封与高危内联确认卡)。
|
||
mgr.SetCommandSource(busAdapter{bus: bus})
|
||
// EXT-11:公开受限 Host 工厂(下游恢复路径经 extension.NewScopedHost 重建句柄)。
|
||
wireScopedHostFactory(caps)
|
||
compiledSummary := applyCompiled(skillsRoot, bus, caps)
|
||
if err := requireProjectContextProvider(opt.RequireProjectContext, contextSlot); err != nil {
|
||
return err
|
||
}
|
||
mgr.SetProjectWorkAuthorization(projectSlot.work())
|
||
if contextSlot.active() {
|
||
mgr.SetProjectContextResolver(contextSlot.resolve)
|
||
mgr.SetProjectEvidenceReader(contextSlot.readEvidence)
|
||
mgr.SetProjectContextSourceRechecker(contextSlot.recheck)
|
||
}
|
||
mgr.SetKnowledgeProjectScope(func(ctx context.Context, actor string, id core.ID) (core.ID, error) {
|
||
return projectSlot.resolve(ctx, caps, actor, id)
|
||
})
|
||
if authzSlot.hasV2() {
|
||
mgr.SetKnowledgeResourceAllow(func(actor string, d core.KnowledgeDoc) bool {
|
||
ok, err := authzSlot.AuthorizeResource(extension.ResourceAuthzQuery{Actor: actor, Action: "model-context", ResourceType: "knowledge", ResourceID: d.ID, Owner: d.Owner, ProjectID: d.ProjectID, Source: d.Source})
|
||
return err == nil && ok
|
||
})
|
||
}
|
||
mgr.OnUsage = func(owner string, tokens int) {
|
||
budget := int(setSvc.Effective(owner, "").GetNumber("llm.dailyTokenBudget", 0))
|
||
status, aerr := st.AccountTokens(owner, tokens, budget)
|
||
if aerr != nil {
|
||
logger.Error("llm budget", aerr, "code", string(errcode.SysInternal))
|
||
return
|
||
}
|
||
if status.Over {
|
||
_ = st.AppendAuditEvent(store.AuditEvent{Owner: owner, Action: "llm.budget_exceeded", Evidence: fmt.Sprintf(`{"total":%d,"budget":%d}`, status.Total, budget)})
|
||
logger.Warn("llm daily budget exceeded", "owner", owner, "total", status.Total, "budget", budget)
|
||
}
|
||
}
|
||
hostSrv.Dispatch = func(ctx context.Context, pluginID, owner, commandID string, params map[string]any) error {
|
||
resp := bus.Dispatch(ctx, cmdbus.Request{ID: commandID, Params: params, Entrance: "api", Actor: pluginID, Owner: owner})
|
||
if resp.Receipt.State == "rejected" || resp.Receipt.State == "unknown" {
|
||
return fmt.Errorf("%s", resp.Receipt.Reason)
|
||
}
|
||
return nil
|
||
}
|
||
hostSrv.Search = func(ctx context.Context, owner, text string) ([]core.RetrievalHit, error) {
|
||
return caps.searchDefault(ctx, owner, text)
|
||
}
|
||
// EXT-8 余项:hostapi 检索面同样吃适配器共享(本人 + 放行的他人项目知识)。
|
||
hostSrv.SharedSearch = func(ctx context.Context, owner, text string) ([]core.RetrievalHit, error) {
|
||
return caps.searchDefault(ctx, owner, text)
|
||
}
|
||
|
||
hostSrv.Complete = func(ctx context.Context, owner, prompt string) (string, error) {
|
||
return caps.complete(ctx, owner, prompt)
|
||
}
|
||
|
||
srv := server.New(st, mgr, reg, profSvc, setSvc, pluginHost)
|
||
projectSlot.wire(srv, caps)
|
||
// EXT-8:装载期注册的资源授权适配器注入读取入口(未注册=默认拒绝,行为不变)。
|
||
srv.SetProjectAuthorizer(authzSlot.Authorize)
|
||
// EXT-8b:资源级适配器注入读面(未注册=server 回落 V1,行为不变)。
|
||
if authzSlot.hasV2() {
|
||
srv.SetResourceAuthorizerV2(authzSlot.AuthorizeResource)
|
||
}
|
||
// EXT-9:宿主可关闭上传自动索引(发布决定移交下游命令层)。
|
||
srv.DisableUploadIndexing = opt.DisableUploadIndexing
|
||
// EXT-12 最小开关:会话卡不镜像进项目公共流(cardSvc 在上方已建)。
|
||
cardSvc.MirrorConvCards = !opt.DisableConvCardMirror
|
||
|
||
srv.Workflows = wfReg
|
||
mgr.SetWorkflows(wfReg)
|
||
srv.Cards = cardSvc
|
||
srv.Surfaces = surfaceSvc
|
||
// WP3-I4:编译插件摘要进 /api/plugins 管理清单(注册面即清单面)。
|
||
srv.CompiledPlugins = compiledSummary
|
||
srv.Bus = bus
|
||
srv.Gate = gate
|
||
srv.Prompts = promptSvc
|
||
srv.WireBuiltinCommands()
|
||
srv.Retriever = pipe
|
||
srv.Indexer = pipe
|
||
|
||
// 限流(阶段 3):默认进程内档;设置 runtime.redisAddr 切原生 Redis 共享档。
|
||
var limiter ratelimit.Limiter = ratelimit.NewMemory()
|
||
if addr := setSvc.Effective(store.OwnerDefault, "").GetString("runtime.redisAddr", ""); addr != "" {
|
||
rl := ratelimit.NewRedis(addr, "", 0)
|
||
if err := rl.Ping(context.Background()); err != nil {
|
||
return fmt.Errorf("redis %s: %w", addr, err)
|
||
}
|
||
defer rl.Close()
|
||
limiter = rl
|
||
log.Printf("rate limit: shared via redis %s", addr)
|
||
}
|
||
srv.Limiter = limiter
|
||
srv.LimitLookup = cachedInt(5*time.Second, func() int {
|
||
return int(setSvc.Effective(store.OwnerDefault, "").GetNumber("runtime.rateLimitPerMin", 0))
|
||
})
|
||
// 认证策略(auto):PG 档默认开启(多用户面不能裸奔);嵌入式档默认关闭(本地单用户)。
|
||
authOn := resolved.AuthEnabled()
|
||
srv.Auth = server.NewAuthenticator(st, authOn)
|
||
if authOn {
|
||
log.Printf("auth: API key required (create with: agent-system apikey create --owner <name>)")
|
||
}
|
||
srv.Subscribe = func(conversationID string) (<-chan core.StreamEvent, func()) {
|
||
return b.Subscribe(conversationID)
|
||
}
|
||
|
||
httpSrv := &http.Server{Addr: opt.Addr, ReadHeaderTimeout: 10 * time.Second, Handler: server.RequestGuard{
|
||
AuthOn: authOn, AllowedHosts: opt.AllowedHosts, Next: srv.Handler(),
|
||
}}
|
||
|
||
go func() {
|
||
log.Printf("agent-system serving on http://%s (data: %s, plugins: %s)", opt.Addr, opt.DataDir, opt.PluginsDir)
|
||
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||
log.Fatalf("http: %v", err)
|
||
}
|
||
}()
|
||
|
||
// PG 档:事件跨实例扇出(NOTIFY/LISTEN;SQLite 档不需要)。
|
||
if st.Dialect() == "postgres" {
|
||
mgr.SetEventNotifier(pgbus.NewNotifier(st).Notify)
|
||
listener, err := pgbus.Start(context.Background(), opt.StorageDSN, st, b)
|
||
if err != nil {
|
||
return fmt.Errorf("pgbus listen: %w", err)
|
||
}
|
||
defer listener.Close(context.Background())
|
||
log.Printf("pgbus: LISTEN agent_events(跨实例事件扇出)")
|
||
}
|
||
|
||
stop := make(chan os.Signal, 1)
|
||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||
<-stop
|
||
log.Printf("shutting down…")
|
||
mgr.InterruptRunning()
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
if err := httpSrv.Shutdown(ctx); err != nil {
|
||
return err
|
||
}
|
||
mgr.Shutdown() // 冲净事件缓冲后再关存储
|
||
return nil
|
||
}
|
||
|
||
// CreateUserAccount CLI 建号。
|
||
// password 空且 generatePassword=false:无初始密码,同事务签发 activate Key,返回明文 Key。
|
||
// generatePassword=true 或显式 password:兼容旧路径(随机/给定密码 + must_change)。
|
||
func CreateUserAccount(storageDSN, dataDir, owner, name, password string, isAdmin, openExisting, generatePassword bool) (string, string, error) {
|
||
var out, opID string
|
||
err := withStoreMode(storageDSN, dataDir, openExisting, func(st *store.Store) error {
|
||
if password != "" || generatePassword {
|
||
pw := password
|
||
if pw == "" {
|
||
pw = server.RandomPassword()
|
||
}
|
||
hash, err := store.HashPassword(pw)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
outc, err := st.CreateActivatedAccount(store.AccountWrite{
|
||
Owner: owner, Name: name, PasswordHash: hash, IsAdmin: isAdmin, MustChange: true,
|
||
Actor: cliActor(), InstanceID: accountInstanceID(), Action: "account.create",
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
out = pw
|
||
opID = outc.OperationID
|
||
return nil
|
||
}
|
||
outc, err := st.CreateActivatedAccount(store.AccountWrite{
|
||
Owner: owner, Name: name, IsAdmin: isAdmin, Purpose: store.LoginKeyPurposeActivate, CreatedBy: "cli",
|
||
Actor: cliActor(), InstanceID: accountInstanceID(), Action: "account.create",
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
out = outc.Plain
|
||
opID = outc.OperationID
|
||
return nil
|
||
})
|
||
return out, opID, err
|
||
}
|
||
|
||
// IssueUserLoginKey CLI 签发激活/恢复 Key。
|
||
func IssueUserLoginKey(storageDSN, dataDir, owner, purpose string, openExisting, revokeAllCreds bool, ttl time.Duration) (string, error) {
|
||
var plain string
|
||
err := withStoreMode(storageDSN, dataDir, openExisting, func(st *store.Store) error {
|
||
if revokeAllCreds {
|
||
outc, err := st.RevokeAllAndIssue(store.AccountWrite{
|
||
Owner: owner, Purpose: purpose, CreatedBy: "cli", TTL: ttl,
|
||
Actor: cliActor(), InstanceID: accountInstanceID(),
|
||
})
|
||
plain = outc.Plain
|
||
return err
|
||
}
|
||
p, _, err := st.IssueLoginKey(owner, purpose, "cli", ttl)
|
||
plain = p
|
||
return err
|
||
})
|
||
return plain, err
|
||
}
|
||
|
||
// RevokeUserLoginKey CLI 撤销未使用登录 Key。
|
||
func RevokeUserLoginKey(storageDSN, dataDir, id string, openExisting bool) error {
|
||
return withStoreMode(storageDSN, dataDir, openExisting, func(st *store.Store) error {
|
||
return st.RevokeLoginKey(id)
|
||
})
|
||
}
|
||
|
||
// BootstrapAdmin 首位管理员引导:单事务串行(KTD12——PG 咨询锁/SQLite 插入后复查),
|
||
// 建号、激活 Key 与审计共同提交;库内已有用户即拒绝接管。
|
||
func BootstrapAdmin(storageDSN, dataDir, owner string, openExisting bool) (string, error) {
|
||
var plain string
|
||
err := withStoreMode(storageDSN, dataDir, openExisting, func(st *store.Store) error {
|
||
outc, err := st.BootstrapFirstAdminWrite(store.AccountWrite{
|
||
Owner: owner, Name: owner, Actor: cliActor(), InstanceID: accountInstanceID(),
|
||
})
|
||
plain = outc.Plain
|
||
return err
|
||
})
|
||
return plain, err
|
||
}
|
||
|
||
// ResetUserPassword CLI 重置密码:单事务写 must_change=1、bump 认证版本、
|
||
// 作废未用登录 Key、吊销全部会话并审计。
|
||
func ResetUserPassword(storageDSN, dataDir, owner, password string, openExisting bool) (string, error) {
|
||
if password == "" {
|
||
password = server.RandomPassword()
|
||
}
|
||
hash, err := store.HashPassword(password)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if err := withStoreMode(storageDSN, dataDir, openExisting, func(st *store.Store) error {
|
||
return st.AdminResetUserPassword(owner, hash, cliActor(), "cli")
|
||
}); err != nil {
|
||
return "", err
|
||
}
|
||
return password, nil
|
||
}
|
||
|
||
// ListUserAccounts CLI 用户列表。
|
||
func ListUserAccounts(storageDSN, dataDir string, openExisting bool) ([]store.AdminUser, error) {
|
||
var out []store.AdminUser
|
||
err := withStoreMode(storageDSN, dataDir, openExisting, func(st *store.Store) error {
|
||
var e error
|
||
out, e = st.ListUsersAdmin()
|
||
return e
|
||
})
|
||
return out, err
|
||
}
|
||
|
||
func withStore(storageDSN, dataDir string, fn func(*store.Store) error) error {
|
||
return withStoreMode(storageDSN, dataDir, false, fn)
|
||
}
|
||
|
||
// withIdentityStore 只打开既有库:不建文件、不建目录、不迁移。
|
||
func withIdentityStore(storageDSN, dataDir string, fn func(*store.Store) error) error {
|
||
st, err := store.OpenIdentity(storageDSN, dataDir)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer st.Close()
|
||
return fn(st)
|
||
}
|
||
|
||
func withStoreMode(storageDSN, dataDir string, _ bool, fn func(*store.Store) error) error {
|
||
var st *store.Store
|
||
var err error
|
||
// DSN 等价安全输入(KTD14):argv 缺省时回落环境变量——凭据不落 shell history。
|
||
if storageDSN == "" {
|
||
storageDSN = os.Getenv("AGENT_SYSTEM_DSN")
|
||
}
|
||
// Account maintenance never initializes or migrates storage; the retained
|
||
// CLI compatibility flag cannot opt this operation into service initialization.
|
||
if storageDSN != "" {
|
||
st, err = store.OpenExistingPostgres(storageDSN, filepath.Join(dataDir, "artifacts"))
|
||
} else {
|
||
st, err = store.OpenExistingSQLite(filepath.Join(dataDir, "agent-system.db"))
|
||
}
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer st.Close()
|
||
if id := strings.TrimSpace(os.Getenv("AGENT_SYSTEM_INSTANCE_ID")); id != "" {
|
||
dbref := strings.TrimSpace(os.Getenv("AGENT_SYSTEM_DBREF"))
|
||
if dbref == "" {
|
||
if storageDSN != "" {
|
||
dbref = "pg"
|
||
} else {
|
||
dbref = "sqlite"
|
||
}
|
||
}
|
||
if err := st.VerifySystemIdentity(id, dbref); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return fn(st)
|
||
}
|
||
|
||
// CreateAPIKey 是 apikey 子命令的实现:在指定存储档上为 owner 创建密钥(明文仅打印一次)。
|
||
func CreateAPIKey(storageDSN, dataDir, owner, name string) (string, error) {
|
||
var plain string
|
||
err := withStoreMode(storageDSN, dataDir, true, func(st *store.Store) error {
|
||
p, e := st.CreateAPIKey(owner, name)
|
||
plain = p
|
||
return e
|
||
})
|
||
return plain, err
|
||
}
|
||
|
||
// ListAPIKeys 列出租户密钥(摘要视图)。
|
||
func ListAPIKeys(storageDSN, dataDir, owner string) ([]store.APIKeyInfo, error) {
|
||
var out []store.APIKeyInfo
|
||
err := withStoreMode(storageDSN, dataDir, true, func(st *store.Store) error {
|
||
if _, err := st.GetUser(owner); err != nil {
|
||
return fmt.Errorf("owner %q 不存在(先 apikey create 创建)", owner)
|
||
}
|
||
var e error
|
||
out, e = st.ListAPIKeys(owner)
|
||
return e
|
||
})
|
||
return out, err
|
||
}
|
||
|
||
// RevokeAPIKey 撤销指定名称密钥。
|
||
func RevokeAPIKey(storageDSN, dataDir, owner, name string) (bool, error) {
|
||
var ok bool
|
||
err := withStoreMode(storageDSN, dataDir, true, func(st *store.Store) error {
|
||
var e error
|
||
ok, e = st.RevokeAPIKeyByName(owner, name)
|
||
return e
|
||
})
|
||
return ok, err
|
||
}
|
||
|
||
// EnsureSystemIdentity CLI:显式建立库内服务身份(svc init / I2→I3 引导)。
|
||
// 只打开既有库,不建库、不迁移。
|
||
func EnsureSystemIdentity(storageDSN, dataDir, instanceID, dbref string) error {
|
||
return withIdentityStore(storageDSN, dataDir, func(st *store.Store) error {
|
||
return st.EnsureSystemIdentity(instanceID, dbref)
|
||
})
|
||
}
|
||
|
||
// VerifySystemIdentity CLI:校验库内服务身份(svc start 前;失败不插入)。
|
||
// 只打开既有库,不建库、不迁移。schemaVersion 取身份行上的值。
|
||
func VerifySystemIdentity(storageDSN, dataDir, instanceID, dbref string) (int, error) {
|
||
var schemaVersion int
|
||
err := withIdentityStore(storageDSN, dataDir, func(st *store.Store) error {
|
||
if err := st.VerifySystemIdentity(instanceID, dbref); err != nil {
|
||
return err
|
||
}
|
||
row, err := st.GetSystemIdentity()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
schemaVersion = row.SchemaVersion
|
||
return nil
|
||
})
|
||
return schemaVersion, err
|
||
}
|
||
|
||
func cliActor() string {
|
||
return fmt.Sprintf("%d", os.Getuid())
|
||
}
|
||
|
||
func accountInstanceID() string {
|
||
return strings.TrimSpace(os.Getenv("AGENT_SYSTEM_INSTANCE_ID"))
|
||
}
|
||
|
||
// cachedInt 带短 TTL 的整型缓存读(限流值热读取免打库)。
|
||
func cachedInt(ttl time.Duration, fn func() int) func() int {
|
||
var mu sync.Mutex
|
||
var val int
|
||
var at time.Time
|
||
return func() int {
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
if time.Since(at) > ttl {
|
||
val = fn()
|
||
at = time.Now()
|
||
}
|
||
return val
|
||
}
|
||
}
|
||
|
||
// splitKeyPool 拆逗号分隔密钥池(trim 空段)。
|
||
// mcpCache 按配置串缓存 MCP Manager:配置不变复用同一实例(后端解析器
|
||
// per-role 缓存会长期持有它,进程型 server 不重复拉起);配置变更时关闭旧
|
||
// Manager(杀掉其进程型 server)再重建。配置错误返回 nil 并留日志——
|
||
// MCP 是可选能力,坏配置禁用该能力而不是阻塞产品启动。
|
||
var (
|
||
mcpMu sync.Mutex
|
||
mcpKey string
|
||
mcpManager *agentmcp.Manager
|
||
)
|
||
|
||
func mcpManagerFor(raw, overrides string) *agentmcp.Manager {
|
||
mcpMu.Lock()
|
||
defer mcpMu.Unlock()
|
||
key := raw + "\n" + overrides
|
||
if raw == "" {
|
||
if mcpManager != nil {
|
||
mcpManager.Close()
|
||
mcpManager = nil
|
||
}
|
||
mcpKey = ""
|
||
return nil
|
||
}
|
||
if key == mcpKey && mcpManager != nil {
|
||
return mcpManager
|
||
}
|
||
var servers []agentmcp.ServerConfig
|
||
if err := json.Unmarshal([]byte(raw), &servers); err != nil {
|
||
log.Printf("squad agent-native: tools.mcp.servers is not valid JSON, MCP disabled: %v", err)
|
||
return nil
|
||
}
|
||
mgr, err := agentmcp.NewManager(servers)
|
||
if err != nil {
|
||
log.Printf("squad agent-native: MCP config invalid, MCP disabled: %v", err)
|
||
return nil
|
||
}
|
||
if err := mgr.ApplyOverrides(overrides); err != nil {
|
||
log.Printf("squad agent-native: MCP overrides invalid, MCP disabled: %v", err)
|
||
return nil
|
||
}
|
||
if mcpManager != nil {
|
||
mcpManager.Close()
|
||
}
|
||
mcpKey, mcpManager = key, mgr
|
||
return mgr
|
||
}
|
||
|
||
func splitKeyPool(s string) []string {
|
||
var out []string
|
||
for _, k := range strings.Split(s, ",") {
|
||
if k = strings.TrimSpace(k); k != "" {
|
||
out = append(out, k)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// busAdapter EXT-14:cmdbus.Bus 适配 runmgr.CommandSource。视图类型转换在此收口
|
||
// (runmgr 不依赖 cmdbus,边不进架构基线);Entrance 由 runmgr 固定为 agent。
|
||
type busAdapter struct{ bus *cmdbus.Bus }
|
||
|
||
func (a busAdapter) CommandTools() []runmgr.CommandTool {
|
||
return a.CommandToolsFor("")
|
||
}
|
||
|
||
func (a busAdapter) CommandToolsFor(owner string) []runmgr.CommandTool {
|
||
src := a.bus.CommandToolsFor(owner)
|
||
out := make([]runmgr.CommandTool, len(src))
|
||
for i, ct := range src {
|
||
out[i] = runmgr.CommandTool{ID: ct.ID, Description: ct.Description, Params: ct.Params, Risk: ct.Risk}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (a busAdapter) DispatchCommand(ctx context.Context, req runmgr.CommandRequest) runmgr.CommandResponse {
|
||
resp := a.bus.Dispatch(ctx, cmdbus.Request{
|
||
ID: req.ID, Params: req.Params, Entrance: req.Entrance,
|
||
Actor: req.Actor, Owner: req.Owner, ConversationID: req.ConversationID,
|
||
})
|
||
return runmgr.CommandResponse{
|
||
State: string(resp.Receipt.State), Reason: resp.Receipt.Reason,
|
||
Payload: resp.Payload, Card: resp.Card != nil,
|
||
}
|
||
}
|