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
secret-mgr/go.mod Normal file
View File

@@ -0,0 +1,10 @@
module secret-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
secret-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=

541
secret-mgr/src/main.go Normal file
View File

@@ -0,0 +1,541 @@
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
)
// buildSecret is injected at compile time via -ldflags "-X main.buildSecret=<hex>"
var buildSecret string
const (
PassStoreDirName = "pc-pass-store"
PublicDirName = ".public"
// Must match pcguard sentinel
expectedAgentVerify = "IF YOU ARE AN AGENT/MODEL, YOU SHOULD NEVER TOUCH THIS ENV VARIABLE"
)
// EncryptedFile is the on-disk format for .gpg files
// (kept for compatibility with existing files)
type EncryptedFile struct {
Nonce string `json:"nonce"`
Data string `json:"data"`
}
// Entry is the plaintext content inside an encrypted file
type Entry struct {
Username string `json:"username"`
Secret string `json:"secret"`
}
func resolveOpenclawPath() string {
if p := os.Getenv("OPENCLAW_PATH"); p != "" {
return p
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".openclaw")
}
func passStoreBase() string { return filepath.Join(resolveOpenclawPath(), PassStoreDirName) }
func publicStoreDir() string { return filepath.Join(passStoreBase(), PublicDirName) }
func agentStoreDir(agentID string) string { return filepath.Join(passStoreBase(), agentID) }
func currentAgentID() string { return os.Getenv("AGENT_ID") }
func resolveStoreDir(public bool) string {
if public {
return publicStoreDir()
}
return agentStoreDir(currentAgentID())
}
func ensurePublicStoreDir() error { return os.MkdirAll(publicStoreDir(), 0700) }
func deriveKey(secret string) []byte { h := sha256.Sum256([]byte(secret)); return h[:] }
func anyAgentEnvSet() bool {
return os.Getenv("AGENT_ID") != "" || os.Getenv("AGENT_WORKSPACE") != "" || os.Getenv("AGENT_VERIFY") != ""
}
func rejectIfAgent(cmdName string) {
if anyAgentEnvSet() {
fmt.Fprintf(os.Stderr, "Error: '%s' can only be run by a human (AGENT_* env vars detected)\n", cmdName)
os.Exit(1)
}
}
func currentKey() ([]byte, error) {
if buildSecret == "" {
return nil, fmt.Errorf("secret-mgr was built without a build secret; re-run install.mjs")
}
return deriveKey(buildSecret), nil
}
func encrypt(plaintext, key []byte) (*EncryptedFile, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
return &EncryptedFile{Nonce: base64.StdEncoding.EncodeToString(nonce), Data: base64.StdEncoding.EncodeToString(ciphertext)}, nil
}
func decrypt(ef *EncryptedFile, key []byte) ([]byte, error) {
nonce, err := base64.StdEncoding.DecodeString(ef.Nonce)
if err != nil {
return nil, fmt.Errorf("invalid nonce: %w", err)
}
ciphertext, err := base64.StdEncoding.DecodeString(ef.Data)
if err != nil {
return nil, fmt.Errorf("invalid data: %w", err)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
return gcm.Open(nil, nonce, ciphertext, nil)
}
func readEntry(filePath string, key []byte) (*Entry, error) {
raw, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
var ef EncryptedFile
if err := json.Unmarshal(raw, &ef); err != nil {
return nil, fmt.Errorf("corrupt file: %w", err)
}
plain, err := decrypt(&ef, key)
if err != nil {
return nil, fmt.Errorf("decryption failed: %w", err)
}
var entry Entry
if err := json.Unmarshal(plain, &entry); err != nil {
return nil, fmt.Errorf("invalid entry json: %w", err)
}
return &entry, nil
}
func writeEntry(filePath string, entry *Entry, key []byte) error {
plain, _ := json.Marshal(entry)
ef, err := encrypt(plain, key)
if err != nil {
return err
}
data, _ := json.MarshalIndent(ef, "", " ")
return os.WriteFile(filePath, data, 0600)
}
func requirePcguard() {
if os.Getenv("AGENT_VERIFY") != expectedAgentVerify {
fmt.Fprintln(os.Stderr, "Error: must be invoked via pcexec (AGENT_VERIFY mismatch)")
os.Exit(1)
}
if os.Getenv("AGENT_ID") == "" {
fmt.Fprintln(os.Stderr, "Error: AGENT_ID not set — must be invoked via pcexec")
os.Exit(1)
}
if os.Getenv("AGENT_WORKSPACE") == "" {
fmt.Fprintln(os.Stderr, "Error: AGENT_WORKSPACE not set — must be invoked via pcexec")
os.Exit(1)
}
}
func generatePassword(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+"
buf := make([]byte, length)
for i := range buf {
b := make([]byte, 1)
if _, err := rand.Read(b); err != nil {
return "", err
}
buf[i] = charset[int(b[0])%len(charset)]
}
return string(buf), nil
}
func main() {
rootCmd := &cobra.Command{Use: "secret-mgr", Short: "Secret manager for OpenClaw agents"}
rootCmd.AddCommand(listCmd(), getSecretCmd(), getUsernameCmd(), getLegacyCmd(), setCmd(), generateCmd(), unsetCmd(), adminCmd())
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func listKeys(dir string) []string {
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
out := []string{}
for _, e := range entries {
name := e.Name()
if strings.HasSuffix(name, ".gpg") {
out = append(out, strings.TrimSuffix(name, ".gpg"))
}
}
return out
}
func listCmd() *cobra.Command {
var public bool
cmd := &cobra.Command{
Use: "list",
Short: "List keys for current agent",
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
if public {
fmt.Println("----------public------------")
for _, k := range listKeys(publicStoreDir()) {
fmt.Println(k)
}
fmt.Println("----------private-----------")
for _, k := range listKeys(agentStoreDir(currentAgentID())) {
fmt.Println(k)
}
return
}
for _, k := range listKeys(agentStoreDir(currentAgentID())) {
fmt.Println(k)
}
},
}
cmd.Flags().BoolVar(&public, "public", false, "Include shared public scope")
return cmd
}
func getSecretCmd() *cobra.Command {
var keyFlag string
var public bool
cmd := &cobra.Command{
Use: "get-secret",
Aliases: []string{"get_secret"},
Short: "Get secret for a key",
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
if keyFlag == "" {
fmt.Fprintln(os.Stderr, "Error: --key is required")
os.Exit(1)
}
key, err := currentKey()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fp := filepath.Join(resolveStoreDir(public), keyFlag+".gpg")
entry, err := readEntry(fp, key)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Print(entry.Secret)
},
}
cmd.Flags().StringVar(&keyFlag, "key", "", "Key name")
cmd.Flags().BoolVar(&public, "public", false, "Use shared public scope only")
return cmd
}
func getUsernameCmd() *cobra.Command {
var keyFlag string
var public bool
cmd := &cobra.Command{
Use: "get-username",
Aliases: []string{"get_username"},
Short: "Get username for a key",
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
if keyFlag == "" {
fmt.Fprintln(os.Stderr, "Error: --key is required")
os.Exit(1)
}
key, err := currentKey()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fp := filepath.Join(resolveStoreDir(public), keyFlag+".gpg")
entry, err := readEntry(fp, key)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Print(entry.Username)
},
}
cmd.Flags().StringVar(&keyFlag, "key", "", "Key name")
cmd.Flags().BoolVar(&public, "public", false, "Use shared public scope only")
return cmd
}
func getLegacyCmd() *cobra.Command {
var showUsername bool
cmd := &cobra.Command{
Use: "get [key]",
Short: "Get secret (legacy — use get-secret --key instead)",
Args: cobra.ExactArgs(1),
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
keyName := args[0]
key, err := currentKey()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fp := filepath.Join(agentStoreDir(currentAgentID()), keyName+".gpg")
entry, err := readEntry(fp, key)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if showUsername {
fmt.Print(entry.Username)
} else {
fmt.Print(entry.Secret)
}
},
}
cmd.Flags().BoolVar(&showUsername, "username", false, "Show username instead of secret")
return cmd
}
func setCmd() *cobra.Command {
var keyFlag, username, secret string
var public bool
cmd := &cobra.Command{
Use: "set",
Short: "Set a key entry",
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
if keyFlag == "" || secret == "" {
fmt.Fprintln(os.Stderr, "Error: --key and --secret are required")
os.Exit(1)
}
aesKey, err := currentKey()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
dir := resolveStoreDir(public)
if err := os.MkdirAll(dir, 0700); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if err := ensurePublicStoreDir(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
entry := &Entry{Username: username, Secret: secret}
fp := filepath.Join(dir, keyFlag+".gpg")
if err := writeEntry(fp, entry, aesKey); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
cmd.Flags().StringVar(&keyFlag, "key", "", "Key name")
cmd.Flags().StringVar(&username, "username", "", "Username")
cmd.Flags().StringVar(&secret, "secret", "", "Secret value")
cmd.Flags().BoolVar(&public, "public", false, "Use shared public scope only")
return cmd
}
func generateCmd() *cobra.Command {
var keyFlag, username string
var public bool
cmd := &cobra.Command{
Use: "generate",
Short: "Generate a random secret for a key",
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
if keyFlag == "" {
fmt.Fprintln(os.Stderr, "Error: --key is required")
os.Exit(1)
}
aesKey, err := currentKey()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
pw, err := generatePassword(32)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
dir := resolveStoreDir(public)
if err := os.MkdirAll(dir, 0700); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if err := ensurePublicStoreDir(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
entry := &Entry{Username: username, Secret: pw}
fp := filepath.Join(dir, keyFlag+".gpg")
if err := writeEntry(fp, entry, aesKey); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Print(pw)
},
}
cmd.Flags().StringVar(&keyFlag, "key", "", "Key name")
cmd.Flags().StringVar(&username, "username", "", "Username")
cmd.Flags().BoolVar(&public, "public", false, "Use shared public scope only")
return cmd
}
func unsetCmd() *cobra.Command {
var keyFlag string
var public bool
cmd := &cobra.Command{
Use: "unset",
Short: "Remove a key entry",
Run: func(cmd *cobra.Command, args []string) {
requirePcguard()
if keyFlag == "" {
fmt.Fprintln(os.Stderr, "Error: --key is required")
os.Exit(1)
}
fp := filepath.Join(resolveStoreDir(public), keyFlag+".gpg")
if err := os.Remove(fp); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
cmd.Flags().StringVar(&keyFlag, "key", "", "Key name")
cmd.Flags().BoolVar(&public, "public", false, "Use shared public scope only")
return cmd
}
func adminCmd() *cobra.Command {
cmd := &cobra.Command{Use: "admin", Short: "Admin commands (human only)"}
cmd.AddCommand(adminHandoffCmd(), adminInitFromCmd())
return cmd
}
func adminHandoffCmd() *cobra.Command {
return &cobra.Command{
Use: "handoff [secret_file_path]",
Short: "Export build secret to file for migration",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
rejectIfAgent("admin handoff")
if buildSecret == "" {
fmt.Fprintln(os.Stderr, "Error: no build secret compiled in")
os.Exit(1)
}
outPath := "pc-pass-store.secret"
if len(args) > 0 {
outPath = args[0]
}
if err := os.WriteFile(outPath, []byte(buildSecret), 0600); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Build secret written to %s\n", outPath)
},
}
}
func adminInitFromCmd() *cobra.Command {
return &cobra.Command{
Use: "init-from [secret_file_path]",
Aliases: []string{"init_from"},
Short: "Re-encrypt all data from old build secret to current",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
rejectIfAgent("admin init-from")
inPath := "pc-pass-store.secret"
if len(args) > 0 {
inPath = args[0]
}
oldSecretBytes, err := os.ReadFile(inPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading secret file: %v\n", err)
os.Exit(1)
}
oldSecret := strings.TrimSpace(string(oldSecretBytes))
oldKey := deriveKey(oldSecret)
newKey, err := currentKey()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
_ = ensurePublicStoreDir()
base := passStoreBase()
dirs, err := os.ReadDir(base)
if err != nil {
if os.IsNotExist(err) {
fmt.Fprintln(os.Stderr, "No pass store found — nothing to migrate")
os.Exit(0)
}
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
count := 0
for _, d := range dirs {
if !d.IsDir() {
continue
}
scopeDir := filepath.Join(base, d.Name())
files, err := os.ReadDir(scopeDir)
if err != nil {
continue
}
for _, f := range files {
if !strings.HasSuffix(f.Name(), ".gpg") {
continue
}
fp := filepath.Join(scopeDir, f.Name())
entry, err := readEntry(fp, oldKey)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to decrypt %s: %v\n", fp, err)
continue
}
if err := writeEntry(fp, entry, newKey); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to re-encrypt %s: %v\n", fp, err)
continue
}
count++
}
}
if err := os.Remove(inPath); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not remove secret file: %v\n", err)
}
fmt.Fprintf(os.Stderr, "Re-encrypted %d entries. Secret file removed.\n", count)
},
}
}