Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: help init plan apply destroy cluster-setup deploy-all deploy-infra deploy-services test clean
.PHONY: k8s-status k8s-start k8s-stop k8s-restart
.PHONY: build start stop status logs
.PHONY: build start stop status logs restart-agent

TERRAFORM_DIR := infrastructure/terraform
KUBECONFIG := $(shell pwd)/.kube/config
Expand Down Expand Up @@ -202,6 +202,12 @@ start:
echo "Required sibling repos: vcli, verifier, feeplugin, app-recurring"; \
exit 1; \
fi
@if [ ! -d "../agent-backend" ]; then \
echo "WARNING: ../agent-backend directory not found — agent-backend will be skipped"; \
fi
@if [ ! -d "../mcp" ]; then \
echo "WARNING: ../mcp directory not found — mcp server will be skipped"; \
fi
@echo "Starting infrastructure (postgres, redis, minio)..."
@docker compose -f $(COMPOSE_FILE) down -v --remove-orphans 2>/dev/null || true
docker compose -f $(COMPOSE_FILE) up -d
Expand All @@ -219,6 +225,10 @@ stop:
@-pkill -9 -f "go run.*cmd/server" 2>/dev/null || true
@-pkill -9 -f "go run.*cmd/scheduler" 2>/dev/null || true
@-pkill -9 -f "go run.*cmd/tx_indexer" 2>/dev/null || true
@-pkill -9 -f "go run.*agent-backend.*cmd/server" 2>/dev/null || true
@-pkill -9 -f "agent-backend-server" 2>/dev/null || true
@-pkill -9 -f "go run.*mcp.*cmd/mcp-server" 2>/dev/null || true
@-pkill -9 -f "mcp-server.*-http" 2>/dev/null || true
@-pkill -9 -f "go-build.*/verifier$$" 2>/dev/null || true
@-pkill -9 -f "go-build.*/worker$$" 2>/dev/null || true
@-pkill -9 -f "go-build.*/server$$" 2>/dev/null || true
Expand All @@ -231,6 +241,31 @@ stop:
@rm -rf ~/.vultisig/vaults/ 2>/dev/null || true
@echo "Stopped and cleaned."

restart-agent:
@echo "Restarting agent-backend..."
@-lsof -ti :8084 | xargs kill -9 2>/dev/null || true
@sleep 1
@if [ ! -d "../agent-backend" ]; then \
echo "ERROR: ../agent-backend directory not found"; \
exit 1; \
fi
@cd ../agent-backend && \
set -a && . ./.env && set +a && \
export DATABASE_DSN="postgres://vultisig:vultisig@localhost:5432/vultisig-agent?sslmode=disable" && \
export REDIS_URI="redis://:vultisig@localhost:6379" && \
export VERIFIER_URL="http://localhost:8080" && \
export LOG_FORMAT="text" && \
export SERVER_PORT="8084" && \
go build -o /tmp/agent-backend-server ./cmd/server && \
/tmp/agent-backend-server > $(CURDIR)/logs/agent-backend.log 2>&1 &
@for i in 1 2 3 4 5 6 7 8 9 10; do \
if curl -s http://localhost:8084/healthz > /dev/null 2>&1; then \
echo "Agent Backend restarted → localhost:8084"; \
break; \
fi; \
sleep 1; \
done

status:
@docker compose -f $(COMPOSE_FILE) ps

Expand All @@ -244,6 +279,8 @@ logs:
@echo " tail -f local/logs/dca-scheduler.log"
@echo " tail -f local/logs/fee-server.log"
@echo " tail -f local/logs/fee-worker.log"
@echo " tail -f local/logs/agent-backend.log"
@echo " tail -f local/logs/mcp-server.log"
@echo ""
@echo "All logs: tail -f local/logs/*.log"

59 changes: 49 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The Vultisig stack is tightly coupled:
- **vcli** depends on verifier (TSS protocols)
- **verifier** depends on recipes (chain abstraction)
- **app-recurring** depends on recipes + verifier (policy execution)
- **agent-backend** depends on verifier (plugin specs, policy suggestions)
- All depend on **go-wrappers** (cryptographic primitives)

Changes in one repo often require changes in others. Docker images create version drift - the local vcli binary may be incompatible with pre-built Docker images due to protocol or signature changes.
Expand Down Expand Up @@ -39,18 +40,20 @@ mkdir vultisig && cd vultisig
git clone https://github.com/vultisig/vcli.git
git clone https://github.com/vultisig/verifier.git
git clone https://github.com/vultisig/app-recurring.git
git clone https://github.com/vultisig/agent-backend.git
git clone https://github.com/vultisig/recipes.git
git clone https://github.com/vultisig/go-wrappers.git
```

Directory structure:
```
vultisig/
├── vcli/ # This tool
├── verifier/ # Policy verification + TSS
├── app-recurring/ # DCA plugin
├── recipes/ # Chain abstraction layer
└── go-wrappers/ # Rust crypto (auto-downloaded, but useful to have)
├── vcli/ # This tool
├── verifier/ # Policy verification + TSS
├── app-recurring/ # DCA + Sends plugins
├── agent-backend/ # AI agent backend (optional)
├── recipes/ # Chain abstraction layer
└── go-wrappers/ # Rust crypto (auto-downloaded, but useful to have)
```

---
Expand All @@ -76,7 +79,8 @@ make start # Starts postgres/redis/minio in Docker, services run natively
`make start`:
1. Starts infrastructure in Docker (postgres, redis, minio)
2. Runs verifier (API + worker) natively with `go run`
3. Runs app-recurring (server + worker + scheduler) natively with `go run`
3. Runs app-recurring (DCA + Sends: server + worker + scheduler) natively with `go run`
4. Runs agent-backend natively with `go run` (if repo present)

Logs: `tail -f local/logs/*.log`

Expand Down Expand Up @@ -149,7 +153,7 @@ make start

This starts:
- Infrastructure in Docker: PostgreSQL, Redis, MinIO
- Services natively: Verifier API/worker, DCA plugin server/worker/scheduler
- Services natively: Verifier API/worker, DCA plugin server/worker/scheduler, Sends plugin server/worker/scheduler, Agent Backend

**Validation:**
```bash
Expand Down Expand Up @@ -437,6 +441,14 @@ make status
## vcli Commands Reference

```bash
# Use production verifier/plugin endpoints for any command
./local/vcli.sh --prod <command> [flags]

# Examples
./local/vcli.sh --prod status
./local/vcli.sh --prod plugin list
./local/vcli.sh --prod policy generate --from eth --to usdc --amount 0.01 --output $(pwd)/local/policies/prod-policy.json

# Vault management (put .vult file in local/keyshares/ first)
./local/vcli.sh vault import --password "password"
./local/vcli.sh vault list
Expand All @@ -461,6 +473,12 @@ make status
./local/vcli.sh status
```

When `--prod` is set, vcli uses:
- Verifier: `https://verifier.vultisig.com`
- DCA (Recurring Swaps): `https://plugin-dca-swap.prod.plugins.vultisig.com`
- Fees: `https://plugin-fees.prod.plugins.vultisig.com`
- Recurring Sends: `https://plugin-dca-send.prod.plugins.vultisig.com`

## Services & Ports

| Service | Port | Notes |
Expand All @@ -475,6 +493,24 @@ make status
| DCA Worker | - | Native (go run) |
| DCA Scheduler | - | Native (go run) |
| DCA TX Indexer | - | Native (go run) |
| Sends Server | 8083 | Native (go run) |
| Sends Worker | - | Native (go run) |
| Sends Scheduler | - | Native (go run) |
| Sends TX Indexer | - | Native (go run) |
| Agent Backend | 8084 | Native (go run), optional |

## Agent Backend Setup

The agent-backend requires an `ANTHROPIC_API_KEY` in its `.env` file. If present, `make start` sources `../agent-backend/.env` and runs it automatically against the shared infrastructure.

```bash
# One-time: copy and fill in your API key
cd ../agent-backend
cp .env.example .env
# Edit .env and set ANTHROPIC_API_KEY
```

If the `agent-backend/` repo is not present, `make start` skips it with a warning.

## Queue Isolation (4-Party TSS)

Expand Down Expand Up @@ -515,12 +551,13 @@ vcli/
└── README.md
```

**Sibling repos (required):**
**Sibling repos:**
```
vultisig/
├── vcli/
├── verifier/
├── app-recurring/
├── verifier/ # Required
├── app-recurring/ # Required
├── agent-backend/ # Optional (skipped if missing)
├── recipes/
└── go-wrappers/
```
Expand Down Expand Up @@ -572,6 +609,8 @@ tail -f local/logs/worker.log # Verifier worker
tail -f local/logs/dca-server.log # DCA plugin server
tail -f local/logs/dca-worker.log # DCA plugin worker
tail -f local/logs/dca-scheduler.log # DCA scheduler
tail -f local/logs/sends-server.log # Sends plugin server
tail -f local/logs/agent-backend.log # Agent backend

# View all logs
tail -f local/logs/*.log
Expand Down
87 changes: 76 additions & 11 deletions local/cmd/vcli/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
"io"
"net/http"
"os"
"strings"
"time"

"github.com/ethereum/go-ethereum/crypto"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -84,6 +86,44 @@ type AuthToken struct {
ExpiresAt time.Time `json:"expires_at"`
}

type authMessagePayload struct {
Message string `json:"message"`
Nonce string `json:"nonce"`
ExpiresAt string `json:"expiresAt"`
Address string `json:"address"`
}

func extractAuthTokenFromResponse(body []byte) (string, error) {
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
return "", err
}

if data, ok := payload["data"].(map[string]any); ok {
if token, ok := data["token"].(string); ok && token != "" {
return token, nil
}
if token, ok := data["access_token"].(string); ok && token != "" {
return token, nil
}
if token, ok := data["jwt"].(string); ok && token != "" {
return token, nil
}
}

if token, ok := payload["token"].(string); ok && token != "" {
return token, nil
}
if token, ok := payload["access_token"].(string); ok && token != "" {
return token, nil
}
if token, ok := payload["jwt"].(string); ok && token != "" {
return token, nil
}

return "", fmt.Errorf("auth response missing token")
}

func runAuthLogin(vaultID, password string) error {
cfg, err := LoadConfig()
if err != nil {
Expand Down Expand Up @@ -118,8 +158,23 @@ func runAuthLogin(vaultID, password string) error {
}
nonce := hex.EncodeToString(nonceBytes)

expiryTime := time.Now().Add(5 * time.Minute)
message := fmt.Sprintf("%s:%d", nonce, expiryTime.Unix())
address, err := deriveEthereumAddressFromPubKey(vault.PublicKeyECDSA)
if err != nil {
return fmt.Errorf("derive address from vault public key: %w", err)
}

expiryTime := time.Now().Add(15 * time.Minute).UTC()
messagePayload := authMessagePayload{
Message: "Sign into Vultisig App Store",
Nonce: nonce,
ExpiresAt: expiryTime.Format(time.RFC3339),
Address: strings.ToLower(address),
}
messageBytes, err := json.Marshal(messagePayload)
if err != nil {
return fmt.Errorf("marshal auth message: %w", err)
}
message := string(messageBytes)

fmt.Printf("Authenticating with verifier...\n")
fmt.Printf(" Vault: %s\n", vault.Name)
Expand All @@ -133,7 +188,10 @@ func runAuthLogin(vaultID, password string) error {
fmt.Println("\nPerforming TSS keysign for authentication...")

derivePath := "m/44'/60'/0'/0/0"
results, err := tss.Keysign(ctx, vault, []string{message}, derivePath, false, password)
ethPrefixedMessage := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), message)
messageHash := crypto.Keccak256([]byte(ethPrefixedMessage))
hexMessage := hex.EncodeToString(messageHash)
results, err := tss.KeysignWithFastVault(ctx, vault, []string{hexMessage}, derivePath, password)
if err != nil {
return fmt.Errorf("TSS keysign failed: %w", err)
}
Expand All @@ -142,7 +200,7 @@ func runAuthLogin(vaultID, password string) error {
return fmt.Errorf("no signature result")
}

signature := results[0].DerSignature
signature := "0x" + results[0].R + results[0].S + results[0].RecoveryID

authReq := map[string]string{
"message": message,
Expand Down Expand Up @@ -175,18 +233,13 @@ func runAuthLogin(vaultID, password string) error {
return fmt.Errorf("authentication failed (%d): %s", resp.StatusCode, string(body))
}

var authResp struct {
Data struct {
Token string `json:"token"`
} `json:"data"`
}
err = json.Unmarshal(body, &authResp)
tokenValue, err := extractAuthTokenFromResponse(body)
if err != nil {
return fmt.Errorf("parse auth response: %w", err)
}

authToken := AuthToken{
Token: authResp.Data.Token,
Token: tokenValue,
PublicKey: vault.PublicKeyECDSA,
ExpiresAt: time.Now().Add(7 * 24 * time.Hour),
}
Expand All @@ -202,6 +255,18 @@ func runAuthLogin(vaultID, password string) error {
return nil
}

func deriveEthereumAddressFromPubKey(publicKeyHex string) (string, error) {
keyBytes, err := hex.DecodeString(strings.TrimPrefix(strings.TrimPrefix(publicKeyHex, "0x"), "0X"))
if err != nil {
return "", fmt.Errorf("decode public key: %w", err)
}
pubKey, err := crypto.DecompressPubkey(keyBytes)
if err != nil {
return "", fmt.Errorf("decompress pubkey: %w", err)
}
return crypto.PubkeyToAddress(*pubKey).Hex(), nil
}

func runAuthStatus() error {
token, err := LoadAuthToken()
if err != nil {
Expand Down
Loading
Loading