Publicize identity CLI as pkg/identity for downstream reuse
ci / vet-and-test (pull_request) Successful in 43s
ci / vet-and-test (pull_request) Successful in 43s
Why: runIdentity lived in cmd/agent-system package main, so downstream combination binaries (Agent-System-RDM) cannot reuse it and their unknown subcommands fall into the serve path; the 2026-10-12 rdm1/rdmcand batch update failures traced back to that capability gap. Evidence: go test ./pkg/identity/... ok; CGO_ENABLED=0 go build ok; behavioral probes (usage rc=2, capabilities v1, ensure/verify roundtrip) recorded in the PR. Impact: identity semantics unchanged for the base binary; adds identity capabilities (v1) and usage-on-unknown contract shared by all products; no storage changes. Refs: Dev PR #474 (svc.sh candidate gate); user root-cause report 2026-10-12 U1 (shared identity interface).
This commit is contained in:
@@ -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 <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
|
||||
}
|
||||
return identity.Run(args)
|
||||
}
|
||||
|
||||
@@ -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 <ensure|verify|capabilities> --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 路径 <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)
|
||||
fmt.Fprintln(os.Stderr, Usage)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user