Skip to content

Adds obol openclaw wallet backup and obol openclaw wallet restore#260

Merged
OisinKyne merged 5 commits intomainfrom
worktree-walletcommands
Mar 6, 2026
Merged

Adds obol openclaw wallet backup and obol openclaw wallet restore#260
OisinKyne merged 5 commits intomainfrom
worktree-walletcommands

Conversation

@OisinKyne
Copy link
Contributor

Still keeps things simple on the singular key situation for the time being.

bussyjd and others added 4 commits March 6, 2026 00:48
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>
@OisinKyne OisinKyne merged commit d35a32b into main Mar 6, 2026
6 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, plus PromptBackupBeforePurge for pre-purge safety
  • internal/openclaw/wallet.go + openclaw.go + integration_test.go: Exports previously private helpers (DeploymentPath, KeystoreVolumePath, WriteWalletMetadata, ReadWalletMetadata)
  • cmd/obol/openclaw.go: Wires up the new wallet backup, wallet restore, and wallet list CLI 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.

Comment on lines +3 to +16
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"
)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Force bool // Overwrite existing wallet
}

// BackupWallet creates a backup of the wallet for the given instance.
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...".

Suggested change
// BackupWallet creates a backup of the wallet for the given instance.
// BackupWalletCmd creates a backup of the wallet for the given instance.

Copilot uses AI. Check for mistakes.
Comment on lines +197 to +199
{
Name: "backup",
Usage: "Back up wallet keys for an OpenClaw instance",
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +212
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)
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants