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:
10
ego-mgr/go.mod
Normal file
10
ego-mgr/go.mod
Normal 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
10
ego-mgr/go.sum
Normal 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
482
ego-mgr/src/main.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user