Publicize identity CLI as pkg/identity for downstream reuse
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:
weichen
2026-10-12 13:10:13 +00:00
parent d1f46c5646
commit 5623000d51
3 changed files with 154 additions and 43 deletions
+4 -43
View File
@@ -14,6 +14,7 @@ import (
"agent-system-backend/internal/app" "agent-system-backend/internal/app"
"agent-system-backend/internal/config" "agent-system-backend/internal/config"
"agent-system-backend/internal/store" "agent-system-backend/internal/store"
"agent-system-backend/pkg/identity"
) )
var version = "0.1.0-dev" var version = "0.1.0-dev"
@@ -347,48 +348,8 @@ func runUser(args []string) int {
} }
// runIdentity:U1 I3 服务实例库内身份(manifest.instance_id ↔ agent_system_identity)。 // 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 { func runIdentity(args []string) int {
if len(args) == 0 { return identity.Run(args)
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
}
} }
+74
View File
@@ -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
}
}
+76
View File
@@ -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)
}
}