ci / vet-and-test (pull_request) Successful in 1m2s
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # pkg/extension/extension.go
570 lines
22 KiB
Go
570 lines
22 KiB
Go
// agent-system:单二进制服务(HTTP/SSE + 内嵌前端 + 双档存储)。
|
||
// 部署约束(设计 4.1 原则 8):非容器化;拷贝即运行。
|
||
// 嵌入式档默认(SQLite + 本地对象目录,零外部服务);--dsn 切换 PostgreSQL 档
|
||
// (多用户/多实例,认证默认开启,apikey 子命令管理密钥)。
|
||
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"agent-system-backend/internal/app"
|
||
"agent-system-backend/internal/config"
|
||
"agent-system-backend/internal/store"
|
||
"agent-system-backend/pkg/extension"
|
||
"agent-system-backend/pkg/identity"
|
||
)
|
||
|
||
var version = "0.1.0-dev"
|
||
|
||
func main() {
|
||
if len(os.Args) > 1 && !strings.HasPrefix(os.Args[1], "-") {
|
||
switch os.Args[1] {
|
||
case "version", "--version":
|
||
if len(os.Args) > 2 && os.Args[2] == "--json" {
|
||
if err := identity.WriteDocument(os.Stdout, "agent-system", version); err != nil {
|
||
fmt.Fprintln(os.Stderr, "version:", err)
|
||
os.Exit(1)
|
||
}
|
||
return
|
||
}
|
||
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:]))
|
||
case "capabilities":
|
||
os.Exit(runCapabilities(os.Args[2:]))
|
||
default:
|
||
fmt.Fprintf(os.Stderr, "agent-system: 未知子命令 %s\n", os.Args[1])
|
||
fmt.Fprintln(os.Stderr, "用法:agent-system <serve|version|identity|user|apikey|config|capabilities> [参数]")
|
||
os.Exit(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=不限;覆盖全部业务查询)")
|
||
extension.RegisterCLIFlags(fs)
|
||
fs.Parse(os.Args[1:])
|
||
handled, hookErr := extension.RunCLIHooks(extension.Options{
|
||
Addr: *addr, DataDir: *data, PluginsDir: *pluginsDir, Mock: *mock,
|
||
StorageDSN: *dsn, AuthMode: *auth, DisableExecTools: *noExec, ExecBackend: *execBackend,
|
||
})
|
||
if hookErr != nil {
|
||
fmt.Fprintln(os.Stderr, "agent-system:", hookErr)
|
||
os.Exit(1)
|
||
}
|
||
if handled {
|
||
return
|
||
}
|
||
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)
|
||
}
|
||
pluginDefaults, merr := extension.MergeHostOptions(extension.Options{})
|
||
if merr != nil {
|
||
fmt.Fprintln(os.Stderr, "agent-system:", merr)
|
||
os.Exit(1)
|
||
}
|
||
disableExec := resolved.DisableExecTools
|
||
if !cliSet["disableExecTools"] {
|
||
disableExec = disableExec || pluginDefaults.DisableExecTools
|
||
}
|
||
execBackendResolved := resolved.ExecBackend
|
||
if !cliSet["execBackend"] && execBackendResolved == "" {
|
||
execBackendResolved = pluginDefaults.ExecBackend
|
||
}
|
||
extOpt := extension.Options{
|
||
Addr: resolved.Addr, DataDir: resolved.DataDir, PluginsDir: resolved.PluginsDir,
|
||
DisableUploadIndexing: pluginDefaults.DisableUploadIndexing,
|
||
DisableConvCardMirror: pluginDefaults.DisableConvCardMirror,
|
||
DisableExecTools: disableExec, ExecBackend: execBackendResolved,
|
||
}
|
||
if err := extension.PrepareCompiled(&extOpt); 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(),
|
||
DisableUploadIndexing: extOpt.DisableUploadIndexing,
|
||
DisableConvCardMirror: extOpt.DisableConvCardMirror,
|
||
DisableExecTools: extOpt.DisableExecTools, ExecBackend: extOpt.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(空 = 嵌入式档)")
|
||
dsnFile := fs.String("dsn-file", "", "0600权限的PostgreSQL DSN文件")
|
||
owner := fs.String("owner", "", "租户名(owner)")
|
||
name := fs.String("name", "default", "密钥名称")
|
||
outFile := fs.String("out-file", "", "把明文密钥独占写入该文件(0600)")
|
||
fs.Parse(args[1:])
|
||
if *owner == "" {
|
||
fmt.Fprintln(os.Stderr, "apikey: --owner 必填")
|
||
return 2
|
||
}
|
||
if sub != "create" && *outFile != "" {
|
||
fmt.Fprintln(os.Stderr, "apikey: 此子命令不生成密钥")
|
||
return 2
|
||
}
|
||
resolvedDSN, err := resolveAccountDSN(*dsn, *dsnFile)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "apikey:", err)
|
||
return 1
|
||
}
|
||
*dsn = resolvedDSN
|
||
switch sub {
|
||
case "create":
|
||
delivery, err := reserveCredentialOutput(*outFile, "")
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "apikey create:", err)
|
||
return 1
|
||
}
|
||
defer delivery.abort()
|
||
key, err := app.CreateAPIKey(*dsn, *data, *owner, *name)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "apikey create:", err)
|
||
return 1
|
||
}
|
||
// 明文仅此一次输出;库中只存 SHA-256 摘要。
|
||
if delivery.file == nil {
|
||
fmt.Println(key) // Existing API-key CLI compatibility; owner grants require a file.
|
||
return 0
|
||
}
|
||
if err := delivery.deliver("api-key", *owner, key, nil); err != nil {
|
||
fmt.Fprintln(os.Stderr, "apikey create:", err)
|
||
return 1
|
||
}
|
||
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
|
||
}
|
||
}
|
||
|
||
// deliverCredential writes only to a new private file. The secret never goes
|
||
// to stdout or an existing path; a JSON payload may include expiresAt.
|
||
func deliverCredential(outPath, kind, owner, plaintext string, expiresAt any) error {
|
||
if outPath == "" {
|
||
return fmt.Errorf("protected credential delivery path required")
|
||
}
|
||
payload := map[string]any{"kind": kind, "owner": owner, "secret": plaintext}
|
||
if expiresAt != nil {
|
||
payload["expiresAt"] = expiresAt
|
||
}
|
||
buf, err := json.MarshalIndent(payload, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := writeExclusiveSecret(outPath, string(buf)); err != nil {
|
||
return err
|
||
}
|
||
fmt.Fprintf(os.Stderr, "凭据已写入 %s(0600,仅此一次生成;勿入库勿入日志)\n", outPath)
|
||
return nil
|
||
}
|
||
|
||
type credentialOutput struct {
|
||
path string
|
||
file *os.File
|
||
json bool
|
||
mutated bool
|
||
}
|
||
|
||
// Reserve the destination before changing the account. O_EXCL also rejects an
|
||
// existing symlink, so an existing secret can never be overwritten.
|
||
func reserveCredentialOutput(rawPath, jsonPath string) (*credentialOutput, error) {
|
||
if rawPath != "" && jsonPath != "" {
|
||
return nil, fmt.Errorf("--out and --out-file are mutually exclusive")
|
||
}
|
||
out := &credentialOutput{}
|
||
out.path, out.json = rawPath, false
|
||
if jsonPath != "" {
|
||
out.path, out.json = jsonPath, true
|
||
}
|
||
if out.path == "" {
|
||
return out, nil
|
||
}
|
||
f, err := os.OpenFile(out.path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out.file = f
|
||
return out, nil
|
||
}
|
||
|
||
func (out *credentialOutput) abort() {
|
||
if out == nil || out.file == nil {
|
||
return
|
||
}
|
||
info, err := out.file.Stat()
|
||
_ = out.file.Close()
|
||
if out.mutated || err != nil {
|
||
return
|
||
}
|
||
pathInfo, pathErr := os.Stat(out.path)
|
||
if pathErr == nil && os.SameFile(info, pathInfo) {
|
||
_ = os.Remove(out.path)
|
||
}
|
||
}
|
||
|
||
func (out *credentialOutput) deliver(kind, owner, plaintext string, expiresAt any) error {
|
||
if out.file == nil {
|
||
return fmt.Errorf("protected credential delivery path required")
|
||
}
|
||
out.mutated = true // Keep the private recovery artifact if delivery fails after DB commit.
|
||
var body []byte
|
||
if out.json {
|
||
payload := map[string]any{"kind": kind, "owner": owner, "secret": plaintext}
|
||
if expiresAt != nil {
|
||
payload["expiresAt"] = expiresAt
|
||
}
|
||
var err error
|
||
body, err = json.MarshalIndent(payload, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
} else {
|
||
body = []byte(plaintext)
|
||
}
|
||
body = append(body, '\n')
|
||
if _, err := out.file.Write(body); err != nil {
|
||
return err
|
||
}
|
||
if err := out.file.Sync(); err != nil {
|
||
return err
|
||
}
|
||
if err := out.file.Close(); err != nil {
|
||
return err
|
||
}
|
||
out.file = nil
|
||
fmt.Fprintf(os.Stderr, "凭据已写入 %s(0600,仅此一次生成;勿入库勿入日志)\n", out.path)
|
||
return nil
|
||
}
|
||
|
||
// runUser:本地 owner 账号初始化、续办和只读/撤销操作。
|
||
func runUser(args []string) int {
|
||
if len(args) == 0 {
|
||
fmt.Fprintln(os.Stderr, "用法:agent-system user <create|initialize-account|list|revoke-login-key|bootstrap-admin> [flags]")
|
||
return 2
|
||
}
|
||
sub := args[0]
|
||
fs := flag.NewFlagSet("user "+sub, flag.ExitOnError)
|
||
data := fs.String("data", "./data", "数据目录")
|
||
dsn := fs.String("dsn", "", "PostgreSQL 档 DSN(空 = 嵌入式档;已弃用,优先 AGENT_SYSTEM_DSN 或 --dsn-file)")
|
||
dsnFile := fs.String("dsn-file", "", "0600 权限的 DSN 文件")
|
||
owner := fs.String("owner", "", "用户名(owner)")
|
||
userID := fs.String("user-id", "", "待初始化账号稳定ID")
|
||
expectedStateVersion := fs.Int64("expected-state-version", 0, "待初始化账号状态版本")
|
||
expectedGeneration := fs.Int64("expected-generation", -1, "待初始化账号代数")
|
||
name := fs.String("name", "", "显示名")
|
||
password := fs.String("password", "", "密码(已弃用;请用 --password-stdin)")
|
||
passwordStdin := fs.Bool("password-stdin", false, "从 stdin 读取密码")
|
||
outFile := fs.String("out-file", "", "把明文凭据独占写入该文件(0600)")
|
||
instanceID := fs.String("instance-id", "", "严格核验库内服务身份")
|
||
admin := fs.Bool("admin", false, "管理员")
|
||
role := fs.String("role", "user", "角色 user|admin")
|
||
openExisting := fs.Bool("open-existing", false, "兼容旧参数;账户命令始终只打开既有存储")
|
||
generatePassword := fs.Bool("generate-password", false, "旧随机初始密码参数(现已拒绝)")
|
||
fs.String("purpose", "recover", "旧 issue-login-key 参数(现已拒绝)")
|
||
fs.Int("ttl-minutes", 30, "旧 issue-login-key 参数(现已拒绝)")
|
||
fs.Bool("revoke-all-credentials", false, "旧 issue-login-key 参数(现已拒绝)")
|
||
keyID := fs.String("id", "", "revoke-login-key 凭据编号")
|
||
out := fs.String("out", "", "凭据保护交付文件(0600 JSON)")
|
||
fs.Parse(args[1:])
|
||
if sub == "reset-password" || sub == "issue-login-key" {
|
||
fmt.Fprintln(os.Stderr, "RECOVERY_CONTRACT_REPLACED: use owner initialization or owner-confirmed recovery")
|
||
return 2 // reject before DSN/password reads, storage opens or output reservation
|
||
}
|
||
if sub == "create" && (*password != "" || *passwordStdin || *generatePassword) {
|
||
fmt.Fprintln(os.Stderr, "RECOVERY_CONTRACT_REPLACED: local creation uses trusted activation only")
|
||
return 2
|
||
}
|
||
resolvedDSN, err := resolveAccountDSN(*dsn, *dsnFile)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "user:", err)
|
||
return 1
|
||
}
|
||
resolvedPassword, err := resolveAccountPassword(*password, *passwordStdin)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "user:", err)
|
||
return 1
|
||
}
|
||
if *instanceID != "" {
|
||
_ = os.Setenv("AGENT_SYSTEM_INSTANCE_ID", *instanceID)
|
||
}
|
||
*dsn = resolvedDSN
|
||
*password = resolvedPassword
|
||
credentialAction := sub == "create" || sub == "initialize-account" || sub == "bootstrap-admin"
|
||
if credentialAction && *outFile == "" && *out == "" {
|
||
fmt.Fprintln(os.Stderr, "user: --out or --out-file is required for protected credential delivery")
|
||
return 2
|
||
}
|
||
if !credentialAction && (*outFile != "" || *out != "") {
|
||
fmt.Fprintln(os.Stderr, "user: 此子命令不生成凭据")
|
||
return 2
|
||
}
|
||
var delivery *credentialOutput
|
||
if credentialAction {
|
||
delivery, err = reserveCredentialOutput(*outFile, *out)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "user:", err)
|
||
return 2
|
||
}
|
||
defer delivery.abort()
|
||
}
|
||
switch sub {
|
||
case "create":
|
||
if *owner == "" {
|
||
fmt.Fprintln(os.Stderr, "user create: --owner 必填")
|
||
return 2
|
||
}
|
||
isAdmin := *admin
|
||
switch *role {
|
||
case "user":
|
||
case "admin":
|
||
isAdmin = true
|
||
default:
|
||
fmt.Fprintf(os.Stderr, "user create: 未知角色 %q\n", *role)
|
||
return 2
|
||
}
|
||
pw, opID, err := app.CreateUserAccount(*dsn, *data, *owner, *name, *password, isAdmin, *openExisting, *generatePassword)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "user create:", err)
|
||
return 1
|
||
}
|
||
if err := delivery.deliver("login-key", *owner, pw, nil); err != nil {
|
||
fmt.Fprintln(os.Stderr, "user create:", err)
|
||
return 1
|
||
}
|
||
fmt.Fprintf(os.Stderr, "operation_id=%s\n", opID)
|
||
return 0
|
||
case "initialize-account":
|
||
if *userID == "" || *expectedStateVersion < 1 || *expectedGeneration < 0 {
|
||
fmt.Fprintln(os.Stderr, "user initialize-account: --user-id、--expected-state-version、--expected-generation 必填")
|
||
return 2
|
||
}
|
||
plain, targetOwner, err := app.InitializeAccount(*dsn, *data, *userID, *expectedStateVersion, *expectedGeneration)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "user initialize-account:", err)
|
||
return 1
|
||
}
|
||
if err := delivery.deliver("activation", targetOwner, plain, time.Now().Add(30*time.Minute).UTC()); err != nil {
|
||
fmt.Fprintln(os.Stderr, "user initialize-account:", err)
|
||
return 1
|
||
}
|
||
return 0
|
||
case "reset-password":
|
||
fmt.Fprintln(os.Stderr, "RECOVERY_CONTRACT_REPLACED")
|
||
return 2
|
||
case "list":
|
||
users, err := app.ListUserAccounts(*dsn, *data, *openExisting)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "user list:", err)
|
||
return 1
|
||
}
|
||
out, _ := json.MarshalIndent(users, "", " ")
|
||
fmt.Println(string(out))
|
||
return 0
|
||
case "issue-login-key":
|
||
fmt.Fprintln(os.Stderr, "RECOVERY_CONTRACT_REPLACED")
|
||
return 2
|
||
case "revoke-login-key":
|
||
if *keyID == "" {
|
||
fmt.Fprintln(os.Stderr, "user revoke-login-key: --id 必填")
|
||
return 2
|
||
}
|
||
if err := app.RevokeUserLoginKey(*dsn, *data, *keyID, *openExisting); err != nil {
|
||
fmt.Fprintln(os.Stderr, "user revoke-login-key:", err)
|
||
return 1
|
||
}
|
||
fmt.Println("revoked")
|
||
return 0
|
||
case "bootstrap-admin":
|
||
if *owner == "" {
|
||
fmt.Fprintln(os.Stderr, "user bootstrap-admin: --owner 必填")
|
||
return 2
|
||
}
|
||
plain, err := app.BootstrapAdmin(*dsn, *data, *owner, *openExisting)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "user bootstrap-admin:", err)
|
||
return 1
|
||
}
|
||
if err := delivery.deliver("bootstrap-admin", *owner, plain, time.Now().Add(30*time.Minute).UTC()); err != nil {
|
||
fmt.Fprintln(os.Stderr, "user bootstrap-admin:", err)
|
||
return 1
|
||
}
|
||
return 0
|
||
default:
|
||
fmt.Fprintln(os.Stderr, "user: 未知子命令", sub)
|
||
return 2
|
||
}
|
||
}
|
||
|
||
// runIdentity:U1 I3 服务实例库内身份(manifest.instance_id ↔ agent_system_identity)。
|
||
// 与 serve --instance(runs 进程标识)无关。实现在 pkg/identity(公共化:下游
|
||
// 组合仓共用同一 CLI,未知子命令 usage 退出、不落服务路径——2026-10-12 裁定)。
|
||
func runIdentity(args []string) int {
|
||
return identity.Run(args)
|
||
}
|