Adds obol openclaw wallet backup and obol openclaw wallet restore#260
Adds obol openclaw wallet backup and obol openclaw wallet restore#260
obol openclaw wallet backup and obol openclaw wallet restore#260Conversation
Adds the full sell→discover→buy BDD test suite and the upstream auth injection mechanism, rebased cleanly on current main. ## BDD Integration Tests (godog/Gherkin) 7 scenarios, 75 steps, following the real user journey: 1. Operator sells inference via CLI + agent reconciles 2. Unpaid request returns 402 with pricing 3. Paid request returns real inference (EIP-712 → verify → Ollama) 4. Full discovery-to-payment cycle 5. Paid request through Cloudflare tunnel 6. Agent discovers registered service through tunnel 7. Operator deletes ServiceOffer + cleanup TestMain bootstrap: obol stack init/up → model setup → sell pricing → agent init → sell http → wait for reconciliation. No kubectl shortcuts. ## Upstream Auth Injection x402-verifier now injects Authorization header on paid requests: - RouteRule.UpstreamAuth field in pricing config - Verifier sets header in 200 response → Traefik copies via authResponseHeaders - monetize.py reads LiteLLM master key → writes upstreamAuth to route - Eliminates manual HTTPRoute RequestHeaderModifier patches ## Tunnel URL Injection `obol tunnel status` auto-sets AGENT_BASE_URL on the obol-agent deployment. monetize.py reads it to publish the tunnel URL in registration JSON. Files: - internal/x402/features/integration_payment_flow.feature (new) - internal/x402/bdd_integration_test.go (new) - internal/x402/bdd_integration_steps_test.go (new) - internal/x402/config.go (UpstreamAuth field) - internal/x402/verifier.go (inject Authorization on 200) - internal/embed/skills/sell/scripts/monetize.py (read master key, upstreamAuth) - internal/tunnel/tunnel.go (InjectBaseURL, auto-inject on status) - internal/embed/infrastructure/.../obol-agent-monetize-rbac.yaml (secrets:get)
The previous commit added secrets:get to the cluster-wide openclaw-monetize-workload ClusterRole, which gave the agent read access to ALL secrets in ALL namespaces. Fix: remove secrets from ClusterRole and add a namespaced Role in the llm namespace scoped to litellm-secrets only via resourceNames restriction. Same pattern as the existing openclaw-x402-pricing Role in the x402 namespace. Verified: - Agent can read litellm-secrets in llm namespace (200 OK) - Agent cannot list kube-system secrets (403 Forbidden) - All 7 BDD scenarios pass with scoped RBAC
Signed-off-by: Oisín Kyne <4981644+OisinKyne@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds wallet backup and restore functionality to the obol openclaw CLI. It introduces obol openclaw wallet backup, obol openclaw wallet restore, and obol openclaw wallet list subcommands, along with a pre-purge safety prompt that warns users about wallets that would be destroyed when running obol stack purge --force. Several previously private helper functions (deploymentPath, keystoreVolumePath, writeWalletMetadata, readWalletMetadata) are exported so the new backup code — in its own file — can access them.
Changes:
internal/openclaw/wallet_backup.go: New file implementing backup/restore logic with optional AES-256-GCM encryption, plusPromptBackupBeforePurgefor pre-purge safetyinternal/openclaw/wallet.go+openclaw.go+integration_test.go: Exports previously private helpers (DeploymentPath,KeystoreVolumePath,WriteWalletMetadata,ReadWalletMetadata)cmd/obol/openclaw.go: Wires up the newwallet backup,wallet restore, andwallet listCLI subcommands
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
internal/openclaw/wallet_backup.go |
Core backup/restore implementation with encryption, purge-prompt logic, and helper functions |
internal/openclaw/wallet_backup_test.go |
Tests for encrypt/decrypt round-trips, force-restore behavior, invalid versions, and more |
internal/openclaw/wallet_test.go |
Updated to use newly exported KeystoreVolumePath, WriteWalletMetadata, ReadWalletMetadata |
internal/openclaw/wallet.go |
Exports KeystoreVolumePath, WriteWalletMetadata, ReadWalletMetadata |
internal/openclaw/openclaw.go |
Exports DeploymentPath; updates all internal callers |
internal/openclaw/integration_test.go |
Updates scaffoldInstance and test helpers to use exported DeploymentPath |
cmd/obol/openclaw.go |
Adds openclawWalletCommand with backup, restore, and list subcommands |
internal/stack/stack.go |
Calls openclaw.PromptBackupBeforePurge before --force purge |
.gitignore |
Ignores auto-generated obol-wallet-backup-*.json and obol-wallet-backup-*.enc files |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import ( | ||
| "crypto/aes" | ||
| "crypto/cipher" | ||
| "crypto/rand" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "github.com/ObolNetwork/obol-stack/internal/config" | ||
| "github.com/ObolNetwork/obol-stack/internal/kubectl" | ||
| "github.com/ObolNetwork/obol-stack/internal/ui" | ||
| "golang.org/x/crypto/scrypt" | ||
| "gopkg.in/yaml.v3" | ||
| ) |
There was a problem hiding this comment.
The import block in wallet_backup.go does not separate the standard library imports from the third-party/project imports with a blank line, which is inconsistent with the import grouping convention used throughout the codebase (e.g., wallet.go, openclaw.go). Standard library imports and external/project imports should be in separate groups.
| Force bool // Overwrite existing wallet | ||
| } | ||
|
|
||
| // BackupWallet creates a backup of the wallet for the given instance. |
There was a problem hiding this comment.
The doc comment above BackupWalletCmd says "BackupWallet creates a backup..." but the actual function name is BackupWalletCmd. The comment should be updated to match the function name: "BackupWalletCmd creates a backup...".
| // BackupWallet creates a backup of the wallet for the given instance. | |
| // BackupWalletCmd creates a backup of the wallet for the given instance. |
| { | ||
| Name: "backup", | ||
| Usage: "Back up wallet keys for an OpenClaw instance", |
There was a problem hiding this comment.
BackupWalletCmd reads only local files (wallet metadata, keystore JSON, and values-remote-signer.yaml) and does not interact with the Kubernetes cluster at all. Requiring kubectl.EnsureCluster before running the backup command means users cannot back up their wallet keys if the cluster is down (which is precisely when they might need a backup most urgently, e.g. before running obol stack purge). The EnsureCluster guard should be removed from the backup action.
| existingWallet, _ := ReadWalletMetadata(deployDir) | ||
| if existingWallet != nil && !opts.Force { | ||
| return fmt.Errorf("instance %q already has a wallet (address: %s)\nUse --force to overwrite", id, existingWallet.Address) | ||
| } | ||
|
|
||
| // Write keystore file. | ||
| keystoreDir := KeystoreVolumePath(cfg, id) | ||
| if err := os.MkdirAll(keystoreDir, 0700); err != nil { | ||
| return fmt.Errorf("failed to create keystore directory: %w", err) | ||
| } | ||
| keystorePath := filepath.Join(keystoreDir, w.KeystoreUUID+".json") | ||
| if err := os.WriteFile(keystorePath, []byte(w.Keystore), 0600); err != nil { | ||
| return fmt.Errorf("failed to write keystore: %w", err) | ||
| } |
There was a problem hiding this comment.
When --force is used to overwrite an existing wallet, the old keystore file (named by the old KeystoreUUID) is not removed from the keystore volume directory. If the restored backup has a different UUID from the existing wallet, both keystore files will be present in the directory. The remote-signer typically loads all *.json keystores it finds, which would result in two active keys after a --force restore. The old keystore file (existingWallet.KeystoreUUID + ".json") should be removed when the restored UUID differs from the existing one.
Still keeps things simple on the singular key situation for the time being.