Files
Agent-System-Backend/cmd/agent-system/main.go
T
weichenandCursor b92eaca0cd
ci / vet-and-test (pull_request) Failing after 1m6s
Add agent_system_identity marker with ensure/verify CLI
Why: U1 I3 needs a DB-side instance identity anchor matching
manifest.instance_id so wrong/missing DB blocks startup (I4).
Evidence: go test ./internal/store/ ./internal/app/; CLI smoke
ensure/verify on SQLite temp dir.
Impact: migration v26; store Ensure/Verify; identity subcommand;
serve checks AGENT_SYSTEM_INSTANCE_ID when set.
Refs: docs/recovery/u1-identity-manifest.md §5.2; Dev feature/recovery-u1-db-identity

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 03:48:13 +00:00

335 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// agent-system:单二进制服务(HTTP/SSE + 内嵌前端 + 双档存储)。
// 部署约束(设计 4.1 原则 8):非容器化;拷贝即运行。
// 嵌入式档默认(SQLite + 本地对象目录,零外部服务);--dsn 切换 PostgreSQL 档
// (多用户/多实例,认证默认开启,apikey 子命令管理密钥)。
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"agent-system-backend/internal/app"
"agent-system-backend/internal/config"
"agent-system-backend/internal/store"
)
var version = "0.1.0-dev"
func main() {
if len(os.Args) > 1 {
switch os.Args[1] {
case "version", "--version":
fmt.Println("agent-system", version)
return
case "serve":
os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
case "apikey":
os.Exit(runAPIKey(os.Args[2:]))
case "config":
os.Exit(runConfig(os.Args[2:]))
case "user":
os.Exit(runUser(os.Args[2:]))
case "identity":
os.Exit(runIdentity(os.Args[2:]))
}
}
fs := flag.NewFlagSet("serve", flag.ExitOnError)
cfgFile := fs.String("config", "", "显式配置文件(JSON,键见 config explain;无自动发现)")
addr := fs.String("addr", "127.0.0.1:8787", "监听地址")
data := fs.String("data", "./data", "数据目录(SQLite 与工件对象目录)")
pluginsDir := fs.String("plugins", "./plugins", "插件目录(声明资产热加载)")
mock := fs.Bool("mock", false, "演示模式(等同 runtime.mock=true,不调用模型)")
dsn := fs.String("dsn", "", "PostgreSQL 档 DSN(空 = 嵌入式 SQLite 档)")
artifacts := fs.String("artifacts", "", "工件对象目录(默认 <data>/artifacts;PG 档多实例时指向共享卷)")
auth := fs.String("auth", "auto", "API key 认证:auto(PG 档开、嵌入式关)| on | off")
instance := fs.String("instance", "", "本进程标识(空 = 主机名:监听地址;启动对账只收口这个实例的 run)")
insecure := fs.Bool("insecure-listen", false, "允许在非回环地址上不开启认证(每次启动都会警告)")
allowedHosts := fs.String("allowed-hosts", "", "逗号分隔的额外主机名,供反向代理的 Host 与 Origin 校验")
noExec := fs.Bool("disable-exec-tools", false, "硬关闭执行原语 run_shell/run_python/register_artifact(用户设置无法覆盖;多用户部署在执行隔离就位前建议开启)")
execBackend := fs.String("exec-backend", "", "执行后端:direct(默认,现状直启)| sandbox(bwrap 隔离档:断网、只读系统根、仅会话目录可写、进程数限额)")
execDepsDir := fs.String("exec-deps-dir", "", "sandbox 档只读依赖层目录(site-packages 等;空=不挂载)")
execWorkspace := fs.String("exec-workspace", "", "sandbox 档工作区模式:空=直写会话目录 | overlay=冻结基底+私有 upper+CAS 回收(S0)")
tier := fs.String("tier", "", "部署档:空=auto(DSN→PG,否则 SQLite)| local(SQLite,不允许 DSN)| team(PG fail-closed:缺 DSN/未认证拒绝启动,PG 故障拒绝就绪)")
pgMaxOpen := fs.Int("pg-max-open", 0, "PG 最大打开连接(0=默认 16)")
pgMaxIdle := fs.Int("pg-max-idle", 0, "PG 最大空闲连接(0=默认 8)")
pgIdleSec := fs.Int("pg-conn-max-idle-sec", 0, "PG 空闲连接回收秒数(0=默认 300)")
pgLifetimeSec := fs.Int("pg-conn-max-lifetime-sec", 0, "PG 连接最长生命周期秒数(0=不限)")
pgQueryTimeoutSec := fs.Int("pg-query-timeout-sec", 0, "PG statement_timeout 秒数(0=不限;覆盖全部业务查询)")
fs.Parse(os.Args[1:])
cliSet := visited(fs)
resolved, err := config.Load(*cfgFile, os.LookupEnv, config.Config{
Addr: *addr, DataDir: *data, PluginsDir: *pluginsDir, Mock: *mock,
StorageDSN: *dsn, ArtifactsDir: *artifacts, AuthMode: *auth, Instance: *instance,
InsecureListen: *insecure, AllowedHostsRaw: *allowedHosts, DisableExecTools: *noExec,
ExecBackend: *execBackend, ExecDepsDir: *execDepsDir, ExecWorkspace: *execWorkspace,
Tier: *tier, PGMaxOpen: *pgMaxOpen, PGMaxIdle: *pgMaxIdle,
PGConnMaxIdleSec: *pgIdleSec, PGConnMaxLifetimeSec: *pgLifetimeSec, PGQueryTimeoutSec: *pgQueryTimeoutSec,
}, cliSet)
if err != nil {
fmt.Fprintln(os.Stderr, "agent-system:", err)
os.Exit(1)
}
if msgs := resolved.TierProblems(); len(msgs) > 0 {
for _, msg := range msgs {
fmt.Fprintln(os.Stderr, "agent-system: tier guard:", msg)
}
os.Exit(1)
}
if err := app.Run(app.Options{
Addr: resolved.Addr, DataDir: resolved.DataDir, PluginsDir: resolved.PluginsDir, ForceMock: resolved.Mock,
StorageDSN: resolved.StorageDSN, ArtifactsDir: resolved.ArtifactsDir, AuthMode: resolved.AuthMode,
InstanceID: resolved.Instance, InsecureListen: resolved.InsecureListen, AllowedHosts: resolved.AllowedHosts(),
DisableExecTools: resolved.DisableExecTools, ExecBackend: resolved.ExecBackend, ExecDepsDir: resolved.ExecDepsDir,
ExecWorkspace: resolved.ExecWorkspace,
Tier: resolved.Tier,
PGPool: store.PoolOptions{
MaxOpenConn: resolved.PGMaxOpen, MaxIdleConn: resolved.PGMaxIdle,
ConnMaxIdleTimeSec: resolved.PGConnMaxIdleSec, ConnMaxLifetimeSec: resolved.PGConnMaxLifetimeSec,
QueryTimeoutSec: resolved.PGQueryTimeoutSec,
},
}); err != nil {
fmt.Fprintln(os.Stderr, "agent-system:", err)
os.Exit(1)
}
}
// visited 把连字符 flag 收成配置字段名,供 Load 的 cliSet 使用。
func visited(fs *flag.FlagSet) map[string]bool {
set := map[string]bool{}
fs.Visit(func(f *flag.Flag) {
name := f.Name
switch name {
case "insecure-listen":
name = "insecureListen"
case "allowed-hosts":
name = "allowedHosts"
case "disable-exec-tools":
name = "disableExecTools"
case "pg-max-open":
name = "pgMaxOpen"
case "pg-max-idle":
name = "pgMaxIdle"
case "pg-conn-max-idle-sec":
name = "pgConnMaxIdleSec"
case "pg-conn-max-lifetime-sec":
name = "pgConnMaxLifetimeSec"
case "pg-query-timeout-sec":
name = "pgQueryTimeoutSec"
}
set[name] = true
})
return set
}
// runConfig:config explain——只读解析诊断(不建库、不写业务目录;DSN 脱敏)。
func runConfig(args []string) int {
if len(args) == 0 || args[0] != "explain" {
fmt.Fprintln(os.Stderr, "用法:agent-system config explain [--config f.json] [--addr …](其余 serve 同名 flag 用于演示覆盖)")
return 2
}
fs := flag.NewFlagSet("config explain", flag.ExitOnError)
cfgFile := fs.String("config", "", "显式配置文件(JSON)")
addr := fs.String("addr", "127.0.0.1:8787", "")
data := fs.String("data", "./data", "")
pluginsDir := fs.String("plugins", "./plugins", "")
mock := fs.Bool("mock", false, "")
dsn := fs.String("dsn", "", "")
artifacts := fs.String("artifacts", "", "")
auth := fs.String("auth", "auto", "")
instance := fs.String("instance", "", "")
insecure := fs.Bool("insecure-listen", false, "")
allowedHosts := fs.String("allowed-hosts", "", "")
noExec := fs.Bool("disable-exec-tools", false, "")
execBackend := fs.String("exec-backend", "", "")
execDepsDir := fs.String("exec-deps-dir", "", "")
execWorkspace := fs.String("exec-workspace", "", "")
tier := fs.String("tier", "", "")
pgMaxOpen := fs.Int("pg-max-open", 0, "")
pgMaxIdle := fs.Int("pg-max-idle", 0, "")
pgIdleSec := fs.Int("pg-conn-max-idle-sec", 0, "")
pgLifetimeSec := fs.Int("pg-conn-max-lifetime-sec", 0, "")
pgQueryTimeoutSec := fs.Int("pg-query-timeout-sec", 0, "")
fs.Parse(args[1:])
resolved, err := config.Load(*cfgFile, os.LookupEnv, config.Config{
Addr: *addr, DataDir: *data, PluginsDir: *pluginsDir, Mock: *mock,
StorageDSN: *dsn, ArtifactsDir: *artifacts, AuthMode: *auth, Instance: *instance,
InsecureListen: *insecure, AllowedHostsRaw: *allowedHosts, DisableExecTools: *noExec,
ExecBackend: *execBackend, ExecDepsDir: *execDepsDir, ExecWorkspace: *execWorkspace,
Tier: *tier, PGMaxOpen: *pgMaxOpen, PGMaxIdle: *pgMaxIdle,
PGConnMaxIdleSec: *pgIdleSec, PGConnMaxLifetimeSec: *pgLifetimeSec, PGQueryTimeoutSec: *pgQueryTimeoutSec,
}, visited(fs))
if err != nil {
fmt.Fprintln(os.Stderr, "agent-system:", err)
return 1
}
resolved.Explain(os.Stdout, *cfgFile)
return 0
}
// runAPIKey:密钥管理子命令(create/list/revoke)。
func runAPIKey(args []string) int {
// 子命令在前、flag 在后:先剥出子命令再解析(Go flag 遇位置参数即停)。
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "用法:agent-system apikey <create|list|revoke> [--owner x --name y]")
return 2
}
sub := args[0]
fs := flag.NewFlagSet("apikey "+sub, flag.ExitOnError)
data := fs.String("data", "./data", "数据目录")
dsn := fs.String("dsn", "", "PostgreSQL 档 DSN(空 = 嵌入式档)")
owner := fs.String("owner", "", "租户名(owner)")
name := fs.String("name", "default", "密钥名称")
fs.Parse(args[1:])
if *owner == "" {
fmt.Fprintln(os.Stderr, "apikey: --owner 必填")
return 2
}
switch sub {
case "create":
key, err := app.CreateAPIKey(*dsn, *data, *owner, *name)
if err != nil {
fmt.Fprintln(os.Stderr, "apikey create:", err)
return 1
}
// 明文仅此一次输出;库中只存 SHA-256 摘要。
fmt.Println(key)
return 0
case "list":
keys, err := app.ListAPIKeys(*dsn, *data, *owner)
if err != nil {
fmt.Fprintln(os.Stderr, "apikey list:", err)
return 1
}
out, _ := json.MarshalIndent(keys, "", " ")
fmt.Println(string(out))
return 0
case "revoke":
ok, err := app.RevokeAPIKey(*dsn, *data, *owner, *name)
if err != nil {
fmt.Fprintln(os.Stderr, "apikey revoke:", err)
return 1
}
if !ok {
fmt.Fprintf(os.Stderr, "apikey revoke: 未找到 owner=%s name=%s 的有效密钥\n", *owner, *name)
return 1
}
fmt.Println("revoked")
return 0
default:
fmt.Fprintln(os.Stderr, "apikey: 未知子命令", sub)
return 2
}
}
// runUser:用户账号管理子命令(管理员发放制,R2)——create / reset-password / list。
// 密码未显式给定时生成随机值,明文仅此一次输出;创建/重置后首次登录强制改密。
func runUser(args []string) int {
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "用法:agent-system user <create|reset-password|list> [--owner x --name y --admin --password p]")
return 2
}
sub := args[0]
fs := flag.NewFlagSet("user "+sub, flag.ExitOnError)
data := fs.String("data", "./data", "数据目录")
dsn := fs.String("dsn", "", "PostgreSQL 档 DSN(空 = 嵌入式档)")
owner := fs.String("owner", "", "用户名(owner)")
name := fs.String("name", "", "显示名")
password := fs.String("password", "", "密码(空 = 随机生成并打印一次)")
admin := fs.Bool("admin", false, "管理员(可管理用户与密钥)")
fs.Parse(args[1:])
switch sub {
case "create":
if *owner == "" {
fmt.Fprintln(os.Stderr, "user create: --owner 必填")
return 2
}
pw, err := app.CreateUserAccount(*dsn, *data, *owner, *name, *password, *admin)
if err != nil {
fmt.Fprintln(os.Stderr, "user create:", err)
return 1
}
fmt.Println(pw)
if *password == "" {
fmt.Fprintln(os.Stderr, "初始密码仅此一次输出;该用户首次登录须修改密码。")
}
return 0
case "reset-password":
if *owner == "" {
fmt.Fprintln(os.Stderr, "user reset-password: --owner 必填")
return 2
}
pw, err := app.ResetUserPassword(*dsn, *data, *owner, *password)
if err != nil {
fmt.Fprintln(os.Stderr, "user reset-password:", err)
return 1
}
fmt.Println(pw)
if *password == "" {
fmt.Fprintln(os.Stderr, "新密码仅此一次输出;该用户下次登录须修改密码(其余会话已吊销)。")
}
return 0
case "list":
users, err := app.ListUserAccounts(*dsn, *data)
if err != nil {
fmt.Fprintln(os.Stderr, "user list:", err)
return 1
}
out, _ := json.MarshalIndent(users, "", " ")
fmt.Println(string(out))
return 0
default:
fmt.Fprintln(os.Stderr, "user: 未知子命令", sub)
return 2
}
}
// runIdentity:U1 I3 服务实例库内身份(manifest.instance_id ↔ agent_system_identity)。
// 与 serve --instance(runs 进程标识)无关。
func runIdentity(args []string) int {
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "用法:agent-system identity <ensure|verify> --instance-id ID [--dbref REF] [--data DIR] [--dsn DSN]")
return 2
}
sub := args[0]
fs := flag.NewFlagSet("identity "+sub, flag.ExitOnError)
data := fs.String("data", "./data", "数据目录(SQLite 路径 <data>/agent-system.db)")
dsn := fs.String("dsn", "", "PostgreSQL 档 DSN(空 = 嵌入式 SQLite)")
instanceID := fs.String("instance-id", "", "服务实例 ID(manifest.instance_id,形如 svc-<group>-…)")
dbref := fs.String("dbref", "", "数据库锚点(空 = sqlite 或 pg)")
fs.Parse(args[1:])
if *instanceID == "" {
fmt.Fprintln(os.Stderr, "identity: --instance-id 必填")
return 2
}
ref := *dbref
if ref == "" {
if *dsn != "" {
ref = "pg"
} else {
ref = "sqlite"
}
}
switch sub {
case "ensure":
if err := app.EnsureSystemIdentity(*dsn, *data, *instanceID, ref); err != nil {
fmt.Fprintln(os.Stderr, "identity ensure:", err)
return 1
}
fmt.Printf("ensured instance_id=%s dbref=%s\n", *instanceID, ref)
return 0
case "verify":
if err := app.VerifySystemIdentity(*dsn, *data, *instanceID, ref); err != nil {
fmt.Fprintln(os.Stderr, "identity verify:", err)
return 1
}
fmt.Printf("verified instance_id=%s dbref=%s\n", *instanceID, ref)
return 0
default:
fmt.Fprintln(os.Stderr, "identity: 未知子命令", sub)
return 2
}
}