Why: The embedded tier starts without auth, and nothing checked Origin or Host, so a public bind or a foreign page could call write APIs. Evidence: go test ./internal/config ./internal/server covering listen policy, origin rejection, and response headers. Impact: Non-loopback without auth exits before opening the database. HTML artifacts get a sandbox CSP. Reverse proxies must list their public host in allowedHosts. Refs: feature/sillytavern-review Co-authored-by: Cursor <cursoragent@cursor.com>
233 lines
8.3 KiB
Go
233 lines
8.3 KiB
Go
// 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"
|
||
)
|
||
|
||
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:]))
|
||
}
|
||
}
|
||
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 校验")
|
||
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,
|
||
}, cliSet)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "agent-system:", err)
|
||
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(),
|
||
}); 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"
|
||
}
|
||
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", "", "")
|
||
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,
|
||
}, 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
|
||
}
|
||
}
|