// Package app 组合根:装配存储(嵌入式/PG 档)、总线、注册表、服务、runmgr、 // 认证、插件宿主与 HTTP 服务。 package app import ( "context" "crypto/rand" "crypto/sha256" "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/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 档工件对象目录(空 = /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 // ExecEnvRoot 分层 Python 环境根(解释器、基础层、扩展层、白名单)。 // 设置后,未显式给出 ExecDepsDir 时使用其中的 current 基础层。 ExecEnvRoot 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 // AdoptInstanceGuard 在数据库被替换或锁文件被重建后,显式重写守卫记录。 AdoptInstanceGuard bool } // 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) } st, releaseStore, err := openServiceStore(opt) if err != nil { return err } defer releaseStore() 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)") } broker, err := wireExecEnv(st, &opt) if err != nil { return fmt.Errorf("exec env: %w", err) } if opt.ExecDepsDir != "" { tools.SetExecDepsDir(opt.ExecDepsDir) log.Printf("exec tools: sandbox deps layer=%s", opt.ExecDepsDir) if err := tools.ExecDoctor(); err != nil { return fmt.Errorf("exec doctor: %w", err) } log.Printf("exec tools: doctor ok (docx/openpyxl/pptx)") } 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, string, bool) { if owner == "" { owner = store.OwnerDefault } if v, ok := st.ActiveSecret(owner, service); ok { return v, "owner", true } // P2 受管:密钥统一迁入 secrets 表后,系统层受管密钥挂在系统行 //(owner=settings.OwnerSystemRow);租户未单独配置时回退系统密钥。 if v, ok := st.ActiveSecret(settings.OwnerSystemRow, service); ok { return v, "system", true } return "", "", false } 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, "") keys := tavilyKeyPool(st, effNow.GetString("tools.tavily.apiKeys", "")) 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.;未命中回落内联文案)。 squadadapter.RoleInstructions = func(kind string) (string, bool) { body, err := promptSvc.AgentPrompt(store.OwnerDefault, "agent.squad."+kind) return body, err == nil } // 技能种子:交付配方与 house-style 落 /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) if keys := tavilyKeyPool(st, effNow.GetString("tools.tavily.apiKeys", "")); 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 } // 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 = 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{}) if broker != nil { mgr.SetExecEnvDecider(func(actor, id string, allow, isolate bool) (string, error) { out, err := broker.Decide(context.Background(), id, actor, false, allow, isolate) if err != nil { return "", err } return out.Detail, nil }) } authOn := resolved.AuthEnabled() effectiveAccount := accountEffectiveAuthorization(st, authOn) mgr.SetAccountEffectiveAuthorization(effectiveAccount, !authOn) 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 预留能力沙箱根:/workspaces///。 mgr.SetWorkspaceRoot(filepath.Join(opt.DataDir, "workspaces")) // 中断子代理续跑(N2 WP5):依赖转录目录(workspaceRoot)与派生请求。 mgr.ResumeInterruptedChildrenChecked(context.Background(), func(ctx context.Context, owner string) error { valid, err := effectiveAccount(ctx, owner) if err != nil { return err } if !valid { return store.ErrAccountForbidden } return nil }) // 收件箱任务卡:事件走会话事件管道(持久化/seq/重放);关闭即 T0 基线。 cardSvc := cardsvc.New(st, mgr.EmitCardEvent) cardSvc.SetAccountCheckedEmitter(mgr.EmitCardEventAccountChecked) cardSvc.SetProjectSharedEmitter(mgr.EmitProjectSharedCardEvent) surfaceSvc := surfacesvc.New(st, mgr.EmitCardEvent) surfaceSvc.SetAccountCheckedEmitter(mgr.EmitCardEventAccountChecked) go func() { // 启动恢复:重发「快照已落盘、事件未发出」的待恢复投影(KTD)。 if n, err := surfaceSvc.RecoverPendingEventsAccountChecked(context.Background(), !authOn); 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() }) bus.SetEffectiveAccountGuard(func(ctx context.Context, actor string) error { valid, err := effectiveAccount(ctx, actor) if err != nil { return err } if !valid { return store.ErrAccountForbidden } return nil }) bus.RequireAccountCheckedWrites(!authOn) // U5:页面动作分发口——surfacesvc.SubmitAction 经命令总线四入口同源分发 //(审计信封、参数校验、DangerHigh 内联确认卡全复用;Confirmed 不透传)。 surfaceSvc.SetDispatcher(func(ctx context.Context, d surfacesvc.ActionDispatch) (surfacesvc.ActionOutcome, error) { return dispatchSurfaceAction(ctx, bus, d) }) // 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{enrich: func(q extension.ResourceAuthzQuery) (extension.ResourceAuthzQuery, error) { account, err := st.GetAccountByOwner(q.Actor) if err != nil { return q, err } snapshot, err := rolePolicySnapshotForUser(st, account.ID) if err != nil { return q, err } q.AuthorityEpoch = snapshot.AuthorityEpoch q.RolePolicy = &snapshot return q, nil }} projectSlot := &projectLifecycleSlot{} workObserverSlot := &projectWorkObserverSlot{} directorySlot, err := newAccountDirectorySlot(st) if err != nil { return err } contextSlot := &projectContextSlot{} caps := hostcaps{projects: projectSlot, workObserver: workObserverSlot, contexts: contextSlot, projectAudit: &projectAuditSlot{}, directory: directorySlot, regulations: ®ulationsSlot{}, st: st, pipe: pipe, set: setSvc, authz: authzSlot, surfaces: surfaceSvc, allowImplicitLocal: !authOn, accountGuard: func(owner string) error { valid, err := effectiveAccount(context.Background(), owner) if err != nil { return err } if !valid { return store.ErrAccountForbidden } return nil }} bus.SetProjectWorkPreflight(func(ctx context.Context, req cmdbus.Request) error { if req.Actor == "" || req.Actor != req.Owner || req.ConversationID == "" || req.AnchorMessageID == "" || req.HostProjectID == "" { return fmt.Errorf("formal project command requires an authenticated conversation source") } conv, err := st.GetConversation(req.Owner, req.ConversationID) if err != nil || string(conv.ProjectID) != req.HostProjectID { return fmt.Errorf("formal project command conversation binding changed") } raw, err := json.Marshal(req.Params) if err != nil { return err } digest := sha256.Sum256(raw) decision, err := preflightProjectMessage(ctx, st, workObserverSlot, projectSlot.work(), req.AnchorMessageID, "project-command:"+req.ID, req.Owner, req.ConversationID, req.HostProjectID, req.ID, hex.EncodeToString(digest[:])) if err != nil { return err } if decision.State != extension.ProjectWorkProceed || decision.DecisionID == "" { return fmt.Errorf("source analysis did not commit a proceed decision") } return nil }) // 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) // EXT-20:法规检索作为内置只读注册表工具暴露给模型(走 inprocHost 审计路径)。 if err := registerRegulationsTools(reg, caps); err != nil { return fmt.Errorf("regulations tools: %w", err) } if err := requireProjectContextProvider(opt.RequireProjectContext, contextSlot); err != nil { return err } mgr.SetProjectWorkAuthorization(projectSlot.work()) wireProjectWorkGate(mgr, st, caps, workObserverSlot, 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 { if err := caps.accountGuard(owner); err != nil { return err } 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) srv.ExecEnv = broker 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 档默认开启(多用户面不能裸奔);嵌入式档默认关闭(本地单用户)。 srv.Auth = server.NewAuthenticator(st, authOn) if authOn { log.Printf("auth: API key required (create with: agent-system apikey create --owner )") } 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 } func assertLocalAccountOwner(st *store.Store, storageDSN, dataDir string) error { if storageDSN == "" { storageDSN = os.Getenv("AGENT_SYSTEM_DSN") } if storageDSN != "" { var isOwner bool err := st.DB().QueryRow(`SELECT current_user = pg_get_userbyid(c.relowner) FROM pg_class c WHERE c.oid='users'::regclass`).Scan(&isOwner) if err != nil || !isOwner { return store.ErrAccountForbidden } return nil } info, err := os.Stat(filepath.Join(dataDir, "agent-system.db")) if err != nil { return err } stat, ok := info.Sys().(*syscall.Stat_t) if !ok || (os.Geteuid() != 0 && stat.Uid != uint32(os.Geteuid())) { return store.ErrAccountForbidden } return nil } // CreateUserAccount is the local owner-only, audited pending-account path. // Password-bearing creation is rejected before storage is opened. func CreateUserAccount(storageDSN, dataDir, owner, name, password string, isAdmin, openExisting, generatePassword bool) (string, string, error) { if password != "" || generatePassword { return "", "", fmt.Errorf("RECOVERY_CONTRACT_REPLACED: local creation uses trusted activation only") } var out store.AccountOutcome err := withStoreMode(storageDSN, dataDir, openExisting, func(st *store.Store) error { if err := assertLocalAccountOwner(st, storageDSN, dataDir); err != nil { return err } _, next, err := st.CreateLocalPendingAccountWithAudit(owner, name, isAdmin, accountInstanceID()) out = next return err }) return out.Plain, out.OperationID, err } // IssueUserLoginKey is retained only as a rejecting compatibility entry. func IssueUserLoginKey(storageDSN, dataDir, owner, purpose string, openExisting, revokeAllCreds bool, ttl time.Duration) (string, error) { return "", fmt.Errorf("RECOVERY_CONTRACT_REPLACED: use owner initialization or owner-confirmed recovery") } // InitializeAccount resumes a specific pending account. The operator identity // comes from the local OS UID; the target is fixed by stable user ID and CAS. func InitializeAccount(storageDSN, dataDir, userID string, expectedStateVersion, expectedGeneration int64) (string, string, error) { var plain, owner string err := withStoreMode(storageDSN, dataDir, true, func(st *store.Store) error { if err := assertLocalAccountOwner(st, storageDSN, dataDir); err != nil { return err } account, err := st.GetAccount(userID) if err != nil { return err } out, err := st.InitializePendingAccountWithAudit(userID, expectedStateVersion, expectedGeneration, accountInstanceID()) plain, owner = out.Plain, account.Owner return err }) return plain, owner, 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 { if err := assertLocalAccountOwner(st, storageDSN, dataDir); err != nil { return err } 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) { return "", fmt.Errorf("RECOVERY_CONTRACT_REPLACED: administrators cannot set another user's password") } // 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 { if err := assertLocalAccountOwner(st, storageDSN, dataDir); err != nil { return err } account, err := st.GetAccountByOwner(owner) if err != nil { return err } if !account.Enabled || account.InitializationState == store.InitializationPending { return store.ErrAccountForbidden } 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 不存在(先建立账号)", 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, } }