feat: rename pass_mgr → secret-mgr, add ego-mgr binary and skill

M1: Rename pass_mgr to secret-mgr
- Rename directory, binary, and Go module
- Update install.mjs to build/install secret-mgr
- Update pcexec.ts to support secret-mgr patterns (with legacy pass_mgr compat)
- Update plugin config schema (passMgrPath → secretMgrPath)
- Create new skills/secret-mgr/SKILL.md
- install.mjs now initializes ego.json on install

M2: Implement ego-mgr binary (Go)
- Agent Scope and Public Scope column management
- Commands: add column/public-column, delete, set, get, show, list columns
- pcexec environment validation (AGENT_VERIFY, AGENT_ID, AGENT_WORKSPACE)
- File locking for concurrent write safety
- Proper exit codes per spec (0-6)
- Agent auto-registration on read/write
- Global column name uniqueness enforcement

M3: ego-mgr Skill
- Create skills/ego-mgr/SKILL.md with usage guide and examples

Ref: REQUIREMENTS_EGO_MGR.md
This commit is contained in:
zhi
2026-03-24 09:36:03 +00:00
parent be0f194f47
commit 98fc3da39c
13 changed files with 821 additions and 132 deletions

10
ego-mgr/go.mod Normal file
View File

@@ -0,0 +1,10 @@
module ego-mgr
go 1.24.0
require github.com/spf13/cobra v1.8.0
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)

10
ego-mgr/go.sum Normal file
View File

@@ -0,0 +1,10 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

482
ego-mgr/src/main.go Normal file
View File

@@ -0,0 +1,482 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"syscall"
"github.com/spf13/cobra"
)
const (
expectedAgentVerify = "IF YOU ARE AN AGENT/MODEL, YOU SHOULD NEVER TOUCH THIS ENV VARIABLE"
egoFileName = "ego.json"
)
// Exit codes per spec
const (
ExitSuccess = 0
ExitUsageError = 1
ExitColumnNotFound = 2
ExitColumnExists = 3
ExitPermission = 4
ExitLockFailed = 5
ExitJSONError = 6
)
// EgoData is the on-disk JSON structure
type EgoData struct {
Columns []string `json:"columns"`
PublicColumns []string `json:"public-columns"`
PublicScope map[string]string `json:"public-scope"`
AgentScope map[string]map[string]string `json:"agent-scope"`
}
func resolveOpenclawPath() string {
if p := os.Getenv("OPENCLAW_PATH"); p != "" {
return p
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".openclaw")
}
func egoFilePath() string {
return filepath.Join(resolveOpenclawPath(), egoFileName)
}
func currentAgentID() string {
return os.Getenv("AGENT_ID")
}
func requirePcguard() {
if os.Getenv("AGENT_VERIFY") != expectedAgentVerify {
fmt.Fprintln(os.Stderr, "Error: must be invoked via pcexec (AGENT_VERIFY mismatch)")
os.Exit(ExitPermission)
}
if os.Getenv("AGENT_ID") == "" {
fmt.Fprintln(os.Stderr, "Error: AGENT_ID not set — must be invoked via pcexec")
os.Exit(ExitPermission)
}
if os.Getenv("AGENT_WORKSPACE") == "" {
fmt.Fprintln(os.Stderr, "Error: AGENT_WORKSPACE not set — must be invoked via pcexec")
os.Exit(ExitPermission)
}
}
// readEgoData reads and parses the ego.json file
func readEgoData() (*EgoData, error) {
fp := egoFilePath()
raw, err := os.ReadFile(fp)
if err != nil {
if os.IsNotExist(err) {
// Return empty structure
return &EgoData{
Columns: []string{},
PublicColumns: []string{},
PublicScope: map[string]string{},
AgentScope: map[string]map[string]string{},
}, nil
}
return nil, fmt.Errorf("failed to read %s: %w", fp, err)
}
var data EgoData
if err := json.Unmarshal(raw, &data); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", fp, err)
}
// Ensure maps are initialized
if data.PublicScope == nil {
data.PublicScope = map[string]string{}
}
if data.AgentScope == nil {
data.AgentScope = map[string]map[string]string{}
}
if data.Columns == nil {
data.Columns = []string{}
}
if data.PublicColumns == nil {
data.PublicColumns = []string{}
}
return &data, nil
}
// writeEgoData writes ego data to ego.json with file locking
func writeEgoData(data *EgoData) error {
fp := egoFilePath()
// Acquire file lock
lockPath := fp + ".lock"
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: cannot create lock file: %v\n", err)
os.Exit(ExitLockFailed)
}
defer func() {
lockFile.Close()
os.Remove(lockPath)
}()
if err := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
fmt.Fprintln(os.Stderr, "Error: failed to acquire file lock (another process is writing)")
os.Exit(ExitLockFailed)
}
defer syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN)
raw, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
if err := os.WriteFile(fp, append(raw, '\n'), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", fp, err)
}
return nil
}
// ensureAgent ensures the current agent has an entry in agent-scope
func ensureAgent(data *EgoData) {
agentID := currentAgentID()
if _, ok := data.AgentScope[agentID]; !ok {
data.AgentScope[agentID] = map[string]string{}
}
}
// isPublicColumn checks if a column name is in public-columns
func isPublicColumn(data *EgoData, name string) bool {
for _, c := range data.PublicColumns {
if c == name {
return true
}
}
return false
}
// isAgentColumn checks if a column name is in columns (agent scope)
func isAgentColumn(data *EgoData, name string) bool {
for _, c := range data.Columns {
if c == name {
return true
}
}
return false
}
// columnExists checks if a column name exists in either scope
func columnExists(data *EgoData, name string) bool {
return isPublicColumn(data, name) || isAgentColumn(data, name)
}
func main() {
rootCmd := &cobra.Command{
Use: "ego-mgr",
Short: "Agent identity/profile manager for OpenClaw",
Long: `ego-mgr manages agent personal information (name, email, timezone, etc.).
Fields can be Agent Scope (per-agent) or Public Scope (shared by all agents).
Examples:
ego-mgr add column name
ego-mgr add public-column timezone --default UTC
ego-mgr set name "小智"
ego-mgr get name
ego-mgr show
ego-mgr list columns
ego-mgr delete name`,
}
rootCmd.AddCommand(addCmd(), deleteCmd(), setCmd(), getCmd(), showCmd(), listCmd())
if err := rootCmd.Execute(); err != nil {
os.Exit(ExitUsageError)
}
}
func addCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add",
Short: "Add a new column",
}
cmd.AddCommand(addColumnCmd(), addPublicColumnCmd())
return cmd
}
func addColumnCmd() *cobra.Command {
var defaultVal string
cmd := &cobra.Command{
Use: "column <column-name>",
Short: "Add an agent-scope column",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
colName := args[0]
data, err := readEgoData()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
if columnExists(data, colName) {
fmt.Fprintf(os.Stderr, "Error: column '%s' already exists\n", colName)
os.Exit(ExitColumnExists)
}
data.Columns = append(data.Columns, colName)
// Set default value for all existing agents
if defaultVal != "" {
for agentID := range data.AgentScope {
data.AgentScope[agentID][colName] = defaultVal
}
}
if err := writeEgoData(data); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
},
}
cmd.Flags().StringVar(&defaultVal, "default", "", "Default value for the column")
return cmd
}
func addPublicColumnCmd() *cobra.Command {
var defaultVal string
cmd := &cobra.Command{
Use: "public-column <column-name>",
Short: "Add a public-scope column",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
colName := args[0]
data, err := readEgoData()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
if columnExists(data, colName) {
fmt.Fprintf(os.Stderr, "Error: column '%s' already exists\n", colName)
os.Exit(ExitColumnExists)
}
data.PublicColumns = append(data.PublicColumns, colName)
if defaultVal != "" {
data.PublicScope[colName] = defaultVal
} else {
data.PublicScope[colName] = ""
}
if err := writeEgoData(data); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
},
}
cmd.Flags().StringVar(&defaultVal, "default", "", "Default value for the column")
return cmd
}
func deleteCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <column-name>",
Short: "Delete a column and all its values",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
colName := args[0]
data, err := readEgoData()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
if !columnExists(data, colName) {
fmt.Fprintf(os.Stderr, "Error: column '%s' does not exist\n", colName)
os.Exit(ExitColumnNotFound)
}
// Remove from public-columns if present
if isPublicColumn(data, colName) {
newCols := []string{}
for _, c := range data.PublicColumns {
if c != colName {
newCols = append(newCols, c)
}
}
data.PublicColumns = newCols
delete(data.PublicScope, colName)
}
// Remove from agent columns if present
if isAgentColumn(data, colName) {
newCols := []string{}
for _, c := range data.Columns {
if c != colName {
newCols = append(newCols, c)
}
}
data.Columns = newCols
// Remove from all agent scopes
for agentID := range data.AgentScope {
delete(data.AgentScope[agentID], colName)
}
}
if err := writeEgoData(data); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
},
}
return cmd
}
func setCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "set <column-name> <value>",
Short: "Set a field value",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
colName := args[0]
value := args[1]
data, err := readEgoData()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
if !columnExists(data, colName) {
fmt.Fprintf(os.Stderr, "Error: column '%s' does not exist (use 'ego-mgr add column' or 'ego-mgr add public-column' first)\n", colName)
os.Exit(ExitColumnNotFound)
}
// Auto-register agent
ensureAgent(data)
if isPublicColumn(data, colName) {
data.PublicScope[colName] = value
} else {
agentID := currentAgentID()
data.AgentScope[agentID][colName] = value
}
if err := writeEgoData(data); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
},
}
return cmd
}
func getCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "get <column-name>",
Short: "Get a field value",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
colName := args[0]
data, err := readEgoData()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
if !columnExists(data, colName) {
fmt.Fprintf(os.Stderr, "Error: column '%s' does not exist\n", colName)
os.Exit(ExitColumnNotFound)
}
// Auto-register agent
ensureAgent(data)
if isPublicColumn(data, colName) {
fmt.Print(data.PublicScope[colName])
} else {
agentID := currentAgentID()
fmt.Print(data.AgentScope[agentID][colName])
}
},
}
return cmd
}
func showCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "show",
Short: "Show all fields and values",
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
data, err := readEgoData()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
// Auto-register agent
ensureAgent(data)
agentID := currentAgentID()
// Print public scope first
for _, col := range data.PublicColumns {
val := data.PublicScope[col]
fmt.Printf("%s: %s\n", col, val)
}
// Then agent scope
for _, col := range data.Columns {
val := ""
if agentData, ok := data.AgentScope[agentID]; ok {
val = agentData[col]
}
fmt.Printf("%s: %s\n", col, val)
}
},
}
return cmd
}
func listCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List information",
}
cmd.AddCommand(listColumnsCmd())
return cmd
}
func listColumnsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "columns",
Short: "List all column names",
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
data, err := readEgoData()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(ExitJSONError)
}
// Print public columns first
for _, col := range data.PublicColumns {
fmt.Println(col)
}
// Then agent columns
for _, col := range data.Columns {
fmt.Println(col)
}
},
}
return cmd
}