feat: implement pass_mgr, pcexec, and safe-restart modules

- Add pass_mgr Go binary with AES-256-GCM encryption
- Add pcexec TypeScript tool with password sanitization
- Add safe-restart module with state machine and API
- Add slash command handler with cooldown support
- Update README with usage documentation
This commit is contained in:
root
2026-03-05 09:27:44 +00:00
parent 10d7e8a6c2
commit 10e1124550
13 changed files with 2037 additions and 0 deletions

12
pass_mgr/go.mod Normal file
View File

@@ -0,0 +1,12 @@
module pass_mgr
go 1.22
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
)

498
pass_mgr/src/main.go Normal file
View File

@@ -0,0 +1,498 @@
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
)
const (
DefaultAlgorithm = "AES-256-GCM"
AdminKeyDir = ".pass_mgr"
AdminKeyFile = ".priv"
SecretsDirName = ".secrets"
)
// EncryptedData represents the structure of encrypted password file
type EncryptedData struct {
Algorithm string `json:"algorithm"`
Nonce string `json:"nonce"`
Data string `json:"data"`
User string `json:"user,omitempty"`
}
// Config holds admin key configuration
type Config struct {
KeyHash string `json:"key_hash"`
Algorithm string `json:"algorithm"`
}
var (
workspaceDir string
agentID string
username string
)
func main() {
rootCmd := &cobra.Command{
Use: "pass_mgr",
Short: "Password manager for OpenClaw agents",
Long: `A secure password management tool using AES-256-GCM encryption.`,
}
// Get environment variables
workspaceDir = os.Getenv("AGENT_WORKSPACE")
agentID = os.Getenv("AGENT_ID")
// Commands
rootCmd.AddCommand(getCmd())
rootCmd.AddCommand(generateCmd())
rootCmd.AddCommand(unsetCmd())
rootCmd.AddCommand(rotateCmd())
rootCmd.AddCommand(adminInitCmd())
rootCmd.AddCommand(setCmd())
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func getCmd() *cobra.Command {
return &cobra.Command{
Use: "get [key]",
Short: "Get password for a key",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
key := args[0]
password, user, err := getPassword(key)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if username {
fmt.Println(user)
} else {
fmt.Println(password)
}
},
}
}
func generateCmd() *cobra.Command {
var user string
cmd := &cobra.Command{
Use: "generate [key]",
Short: "Generate a new password",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
key := args[0]
// Check if agent is trying to set password
if os.Getenv("AGENT") != "" || os.Getenv("AGENT_WORKSPACE") != "" {
fmt.Fprintln(os.Stderr, "Error: Agents cannot set passwords. Use generate instead.")
os.Exit(1)
}
password, err := generatePassword(32)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if err := setPassword(key, user, password); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(password)
},
}
cmd.Flags().StringVar(&user, "username", "", "Username associated with the password")
return cmd
}
func unsetCmd() *cobra.Command {
return &cobra.Command{
Use: "unset [key]",
Short: "Remove a password",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
key := args[0]
if err := removePassword(key); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
}
func rotateCmd() *cobra.Command {
return &cobra.Command{
Use: "rotate [key]",
Short: "Rotate password for a key",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
key := args[0]
// Check if initialized
if !isInitialized() {
fmt.Fprintln(os.Stderr, "Error: pass_mgr not initialized. Run 'pass_mgr admin init' first.")
os.Exit(1)
}
// Get current user if exists
_, user, err := getPassword(key)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Generate new password
newPassword, err := generatePassword(32)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if err := setPassword(key, user, newPassword); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(newPassword)
},
}
}
func adminInitCmd() *cobra.Command {
var keyPath string
cmd := &cobra.Command{
Use: "admin init",
Short: "Initialize pass_mgr with admin key",
Run: func(cmd *cobra.Command, args []string) {
if err := initAdmin(keyPath); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println("pass_mgr initialized successfully")
},
}
cmd.Flags().StringVar(&keyPath, "key-path", "", "Path to admin key file (optional)")
return cmd
}
func setCmd() *cobra.Command {
var user string
cmd := &cobra.Command{
Use: "set [key] [password]",
Short: "Set password (admin only)",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
// Check if agent is trying to set password
if os.Getenv("AGENT") != "" || os.Getenv("AGENT_WORKSPACE") != "" {
fmt.Fprintln(os.Stderr, "Error: Agents cannot set passwords. Only humans can use 'set'.")
os.Exit(1)
}
key := args[0]
password := args[1]
if err := setPassword(key, user, password); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
cmd.Flags().StringVar(&user, "username", "", "Username associated with the password")
return cmd
}
// Helper functions
func getHomeDir() string {
home, err := os.UserHomeDir()
if err != nil {
return "."
}
return home
}
func getAdminKeyPath() string {
return filepath.Join(getHomeDir(), AdminKeyDir, AdminKeyFile)
}
func getConfigPath() string {
return filepath.Join(getHomeDir(), AdminKeyDir, "config.json")
}
func isInitialized() bool {
_, err := os.Stat(getConfigPath())
return err == nil
}
func loadAdminKey() ([]byte, error) {
keyPath := getAdminKeyPath()
key, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to load admin key: %w", err)
}
// Hash the key to get 32 bytes for AES-256
hash := sha256.Sum256(key)
return hash[:], nil
}
func initAdmin(keyPath string) error {
homeDir := getHomeDir()
adminDir := filepath.Join(homeDir, AdminKeyDir)
// Create admin directory
if err := os.MkdirAll(adminDir, 0700); err != nil {
return fmt.Errorf("failed to create admin directory: %w", err)
}
var key []byte
if keyPath != "" {
// Read provided key
var err error
key, err = os.ReadFile(keyPath)
if err != nil {
return fmt.Errorf("failed to read key file: %w", err)
}
} else {
// Generate new key
key = make([]byte, 32)
if _, err := rand.Read(key); err != nil {
return fmt.Errorf("failed to generate key: %w", err)
}
}
// Save key
keyFile := filepath.Join(adminDir, AdminKeyFile)
if err := os.WriteFile(keyFile, key, 0600); err != nil {
return fmt.Errorf("failed to save key: %w", err)
}
// Save config
config := Config{
KeyHash: fmt.Sprintf("%x", sha256.Sum256(key)),
Algorithm: DefaultAlgorithm,
}
configData, _ := json.MarshalIndent(config, "", " ")
configPath := filepath.Join(adminDir, "config.json")
if err := os.WriteFile(configPath, configData, 0600); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}
func getSecretsDir() string {
if workspaceDir != "" && agentID != "" {
return filepath.Join(workspaceDir, SecretsDirName, agentID)
}
// Fallback to home directory
return filepath.Join(getHomeDir(), SecretsDirName, "default")
}
func getPasswordFilePath(key string) string {
return filepath.Join(getSecretsDir(), key+".gpg")
}
func encrypt(plaintext []byte, key []byte) (*EncryptedData, 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(nonce, nonce, plaintext, nil)
return &EncryptedData{
Algorithm: DefaultAlgorithm,
Nonce: base64.StdEncoding.EncodeToString(nonce),
Data: base64.StdEncoding.EncodeToString(ciphertext[gcm.NonceSize():]),
}, nil
}
func decrypt(data *EncryptedData, key []byte) ([]byte, error) {
ciphertext, err := base64.StdEncoding.DecodeString(data.Data)
if err != nil {
return nil, err
}
nonce, err := base64.StdEncoding.DecodeString(data.Nonce)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
func setPassword(key, user, password string) error {
if !isInitialized() {
return fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
}
adminKey, err := loadAdminKey()
if err != nil {
return err
}
// Create secrets directory
secretsDir := getSecretsDir()
if err := os.MkdirAll(secretsDir, 0700); err != nil {
return fmt.Errorf("failed to create secrets directory: %w", err)
}
// Encrypt password
data := map[string]string{
"password": password,
"user": user,
}
plaintext, _ := json.Marshal(data)
encrypted, err := encrypt(plaintext, adminKey)
if err != nil {
return fmt.Errorf("failed to encrypt: %w", err)
}
encrypted.User = user
// Save to file
filePath := getPasswordFilePath(key)
fileData, _ := json.MarshalIndent(encrypted, "", " ")
if err := os.WriteFile(filePath, fileData, 0600); err != nil {
return fmt.Errorf("failed to save password: %w", err)
}
return nil
}
func getPassword(key string) (string, string, error) {
if !isInitialized() {
return "", "", fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
}
adminKey, err := loadAdminKey()
if err != nil {
return "", "", err
}
filePath := getPasswordFilePath(key)
fileData, err := os.ReadFile(filePath)
if err != nil {
return "", "", fmt.Errorf("password not found: %w", err)
}
var encrypted EncryptedData
if err := json.Unmarshal(fileData, &encrypted); err != nil {
return "", "", fmt.Errorf("failed to parse password file: %w", err)
}
plaintext, err := decrypt(&encrypted, adminKey)
if err != nil {
return "", "", fmt.Errorf("failed to decrypt: %w", err)
}
var data map[string]string
if err := json.Unmarshal(plaintext, &data); err != nil {
return "", "", fmt.Errorf("failed to parse decrypted data: %w", err)
}
return data["password"], data["user"], nil
}
func removePassword(key string) error {
if !isInitialized() {
return fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
}
filePath := getPasswordFilePath(key)
if err := os.Remove(filePath); err != nil {
return fmt.Errorf("failed to remove password: %w", err)
}
return nil
}
func generatePassword(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
password := make([]byte, length)
for i := range password {
randomByte := make([]byte, 1)
if _, err := rand.Read(randomByte); err != nil {
return "", err
}
password[i] = charset[int(randomByte[0])%len(charset)]
}
return string(password), nil
}
// CheckForAdminLeak checks if admin password appears in message/tool calling
func CheckForAdminLeak(content string) bool {
// This is a placeholder - actual implementation should check against actual admin password
// This function should be called by the plugin to monitor messages
configPath := getConfigPath()
if _, err := os.Stat(configPath); err != nil {
return false
}
// TODO: Implement actual leak detection
// For now, just check if content contains common patterns
return strings.Contains(content, "admin") && strings.Contains(content, "password")
}
// ResetOnLeak resets pass_mgr to uninitialized state and logs security breach
func ResetOnLeak() error {
configPath := getConfigPath()
// Remove config (but keep key file for potential recovery)
if err := os.Remove(configPath); err != nil {
return err
}
// Log security breach
logPath := filepath.Join(getHomeDir(), AdminKeyDir, "security_breach.log")
logEntry := fmt.Sprintf("[%s] CRITICAL: Admin password leaked! pass_mgr reset to uninitialized state.\n",
time.Now().Format(time.RFC3339))
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString(logEntry); err != nil {
return err
}
return nil
}