diff --git a/cmd/agent-system/main.go b/cmd/agent-system/main.go index ede0b45..391d27a 100644 --- a/cmd/agent-system/main.go +++ b/cmd/agent-system/main.go @@ -14,6 +14,7 @@ import ( "agent-system-backend/internal/app" "agent-system-backend/internal/config" "agent-system-backend/internal/store" + "agent-system-backend/pkg/identity" ) var version = "0.1.0-dev" @@ -347,48 +348,8 @@ func runUser(args []string) int { } // runIdentity:U1 I3 服务实例库内身份(manifest.instance_id ↔ agent_system_identity)。 -// 与 serve --instance(runs 进程标识)无关。 +// 与 serve --instance(runs 进程标识)无关。实现在 pkg/identity(公共化:下游 +// 组合仓共用同一 CLI,未知子命令 usage 退出、不落服务路径——2026-10-12 裁定)。 func runIdentity(args []string) int { - if len(args) == 0 { - fmt.Fprintln(os.Stderr, "用法:agent-system identity --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 路径 /agent-system.db)") - dsn := fs.String("dsn", "", "PostgreSQL 档 DSN(空 = 嵌入式 SQLite)") - instanceID := fs.String("instance-id", "", "服务实例 ID(manifest.instance_id,形如 svc--…)") - 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 - } + return identity.Run(args) } diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go new file mode 100644 index 0000000..8e525b0 --- /dev/null +++ b/pkg/identity/identity.go @@ -0,0 +1,74 @@ +// Package identity 提供服务实例库内身份子命令的公共 CLI(U1 I3: +// manifest.instance_id ↔ agent_system_identity 表)。 +// 独立可导出包:下游组合仓(Agent-System-RDM 等)与基座二进制共用同一实现, +// identity 语义跨产物一致。契约(2026-10-12 rdm1/rdmcand 事故后确立): +// - 无参/未知子命令打印用法并以非零码退出,绝不落入服务启动路径—— +// 部署侧能力探测必须无副作用; +// - `identity capabilities` 输出版本化能力清单,供探测优先消费。 +package identity + +import ( + "flag" + "fmt" + "os" + + "agent-system-backend/internal/app" +) + +// Usage 无参/未知子命令时打印到 stderr 的用法行。探测契约:同时含 "identity" +// 与 "ensure" 字样(svc.sh svc_bin_has_identity 按 usage 形态判有能力)。 +const Usage = "用法:identity --instance-id ID [--dbref REF] [--data DIR] [--dsn DSN]" + +// Capabilities 版本化能力输出(新增子命令在此登记并递增 vN)。 +const Capabilities = "identity-capabilities: v1 ensure,verify" + +// Run 执行 identity 子命令;args 不含 "identity" 自身,返回进程退出码。 +func Run(args []string) int { + if len(args) == 0 { + fmt.Fprintln(os.Stderr, Usage) + return 2 + } + sub := args[0] + if sub == "capabilities" { + fmt.Println(Capabilities) + return 0 + } + fs := flag.NewFlagSet("identity "+sub, flag.ExitOnError) + data := fs.String("data", "./data", "数据目录(SQLite 路径 /agent-system.db)") + dsn := fs.String("dsn", "", "PostgreSQL 档 DSN(空 = 嵌入式 SQLite)") + instanceID := fs.String("instance-id", "", "服务实例 ID(manifest.instance_id,形如 svc--…)") + 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) + fmt.Fprintln(os.Stderr, Usage) + return 2 + } +} diff --git a/pkg/identity/identity_test.go b/pkg/identity/identity_test.go new file mode 100644 index 0000000..298e9f8 --- /dev/null +++ b/pkg/identity/identity_test.go @@ -0,0 +1,76 @@ +package identity + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" +) + +// capture 把 stdout/stderr 重定向到管道并在 fn 返回后给出捕获文本。 +func capture(t *testing.T, fn func()) (stdout, stderr string) { + t.Helper() + oldOut, oldErr := os.Stdout, os.Stderr + outR, outW, err1 := os.Pipe() + errR, errW, err2 := os.Pipe() + if err1 != nil || err2 != nil { + t.Fatalf("pipe: %v %v", err1, err2) + } + os.Stdout, os.Stderr = outW, errW + defer func() { os.Stdout, os.Stderr = oldOut, oldErr }() + done := make(chan struct{}) + go func() { + defer close(done) + bo, _ := io.ReadAll(outR) + be, _ := io.ReadAll(errR) + stdout, stderr = string(bo), string(be) + }() + fn() + outW.Close() + errW.Close() + <-done + return stdout, stderr +} + +// 探测契约(svc.sh svc_bin_has_identity):无参输出 usage 形态——同时含 +// "identity" 与 "ensure",无服务态痕迹;未知子命令同样不落服务路径。 +func TestUsageAndUnknownExitWithoutServe(t *testing.T) { + out, errS := capture(t, func() { Run(nil) }) + _ = out + if !strings.Contains(errS, "identity") || !strings.Contains(errS, "ensure") { + t.Fatalf("usage 须同时含 identity 与 ensure:%q", errS) + } + if rc := Run(nil); rc != 2 { + t.Fatalf("无参 rc=%d,期望 2", rc) + } + if rc := Run([]string{"bogus", "--instance-id", "x"}); rc != 2 { + t.Fatalf("未知子命令 rc=%d,期望 2", rc) + } +} + +func TestCapabilities(t *testing.T) { + out, _ := capture(t, func() { Run([]string{"capabilities"}) }) + if !strings.HasPrefix(out, "identity-capabilities: v1") || + !strings.Contains(out, "ensure") || !strings.Contains(out, "verify") { + t.Fatalf("capabilities 输出异常:%q", out) + } +} + +func TestEnsureVerifyRoundTripSQLite(t *testing.T) { + dir := t.TempDir() + args := []string{"--data", filepath.Join(dir, "d1")} + id := "svc-t1-abcdef123456" + if rc := Run(append([]string{"ensure", "--instance-id", id}, args...)); rc != 0 { + t.Fatalf("ensure rc=%d", rc) + } + if rc := Run(append([]string{"verify", "--instance-id", id}, args...)); rc != 0 { + t.Fatalf("verify rc=%d", rc) + } + if rc := Run(append([]string{"verify", "--instance-id", "svc-t1-zzzzzz999999"}, args...)); rc != 1 { + t.Fatalf("错 id 须 rc=1,得 %d", rc) + } + if rc := Run(append([]string{"verify", "--instance-id", id, "--dbref", "pg"}, args...)); rc != 1 { + t.Fatalf("错 dbref 须 rc=1,得 %d", rc) + } +}