merobox 0.3.7
A Python CLI tool for managing Calimero nodes in Docker containers
License: MIT
Requires Python: <3.12,>=3.9

# Merobox CLI

A comprehensive Python CLI tool for managing Calimero nodes in Docker containers and executing complex blockchain workflows.

## 📚 Table of Contents

- [🚀 Quick Start](#-quick-start)
- [✨ Features](#-features)
- [🔐 Auth Service Integration](#-auth-service-integration)
- [🌐 Remote Nodes](#-remote-nodes)
- [📖 Workflow Guide](#-workflow-guide)
- [🎯 Local Blockchain Environments](#-local-blockchain-environments)
- [🔧 API Reference](#-api-reference)
- [🛠️ Development Guide](#️-development-guide)
- [❓ Troubleshooting](#-troubleshooting)
- [🏗️ Project Structure](#️-project-structure)
- [📋 Requirements](#-requirements)
- [🚀 Releases & Publishing](#-releases--publishing)
- [🤝 Contributing](#-contributing)
- [📄 License](#-license)
- [🆘 Support](#-support)

## 🚀 Quick Start

### Installation

**APT (Ubuntu/Debian):**

```bash
curl -fsSL https://calimero-network.github.io/merobox/gpg.key \
  | sudo tee /usr/share/keyrings/merobox.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/merobox.gpg] https://calimero-network.github.io/merobox stable main" \
  | sudo tee /etc/apt/sources.list.d/merobox.list
sudo apt update
sudo apt install merobox
```

**PyPI:**

```bash
pipx install merobox
```

**Homebrew:**

```bash
brew install merobox
```

**From source:**

```bash
git clone https://github.com/calimero-network/merobox.git
cd merobox
pipx install -e .
```

### Basic Usage

```bash
# Start Calimero nodes
merobox run --count 2

# Start nodes with authentication service (Docker mode)
merobox run --auth-service

# Start nodes with embedded auth (binary mode)
merobox run --no-docker --binary-path /path/to/merod --auth-mode embedded

# Check node status
merobox list
merobox health

# Execute a workflow
merobox bootstrap run workflow.yml

# By default, workflows use a local NEAR sandbox (no testnet tokens required; contracts are downloaded automatically).
# To use the relayer/testnet instead, pass --enable-relayer:
merobox bootstrap run workflow.yml --enable-relayer

# Stop all nodes and auth services
merobox stop --all
```

## ✨ Features

- **Node Management**: Start, stop, and monitor Calimero nodes in Docker
- **Remote Node Support**: Connect to remote Calimero nodes with user/password or API key authentication
- **Auth Service Integration**: Traefik proxy and authentication service with nip.io DNS
- **Workflow Orchestration**: Execute complex multi-step workflows with YAML
- **Context Management**: Create and manage blockchain contexts
- **Identity Management**: Generate and manage cryptographic identities
- **Function Calls**: Execute smart contract functions via JSON-RPC
- **Dynamic Variables**: Advanced placeholder resolution with embedded support
- **Local NEAR Sandbox (default)**: Use a local NEAR Sandbox for zero-cost testing; contracts are downloaded automatically. Use `--enable-relayer` to use the relayer/testnet instead.

---

## 🔐 Authentication

Merobox supports two authentication modes for securing your Calimero nodes:

1. **Auth Service (Docker Mode)**: Traefik proxy with external authentication service - best for production deployments
2. **Embedded Auth (Binary Mode)**: Built-in authentication directly in merod - best for local development and testing

### Auth Service Integration (Docker Mode)

Merobox supports integrated authentication services with Traefik proxy and nip.io DNS resolution, enabling secure access to your Calimero nodes through web URLs.

### Quick Start with Auth Service

```bash
# Start a single node with auth service
merobox run --auth-service

# Start multiple nodes with auth service
merobox run --count 2 --auth-service

# Stop everything (nodes + auth services)
merobox stop --all
```

### What Gets Created

When you enable `--auth-service`, merobox automatically creates:

1. **Traefik Proxy** (`proxy` container) - Routes traffic and applies middleware
2. **Auth Service** (`auth` container) - Handles authentication and authorization
3. **Docker Networks**:
   - `calimero_web` - External communication (Traefik ↔ Internet)
   - `calimero_internal` - Secure backend communication (Auth ↔ Nodes)

### URL Access

**With Auth Service:**

- Node URLs: `http://node1.127.0.0.1.nip.io`, `http://node2.127.0.0.1.nip.io`, etc.
- Auth Login: `http://node1.127.0.0.1.nip.io/auth/login`
- Admin Dashboard: `http://node1.127.0.0.1.nip.io/admin-dashboard`

**Without Auth Service:**

- Admin Dashboard: `http://localhost:2528/admin-dashboard`
- Admin API: `http://localhost:2528/admin-api/`

### Workflow Integration

Enable auth service in workflows by adding `auth_service: true`:

```yaml
name: "My Auth Workflow"
description: "Workflow with authentication enabled"

# Enable auth service
auth_service: true

nodes:
  count: 1
  base_port: 2428
  base_rpc_port: 2528
  chain_id: "testnet-1"

steps:
  - name: "Wait for startup"
    type: "wait"
    seconds: 5
```

### Architecture

```
Internet → Traefik (port 80) → Node Containers (calimero_web network)
                              ↓
                           Auth Service (calimero_internal network)
```

- **Public routes**: `/admin-dashboard` (no auth required)
- **Protected routes**: `/admin-api/`, `/jsonrpc`, `/ws` (auth required)
- **Auth routes**: `/auth/login`, `/admin/` (handled by auth service)

### Embedded Authentication (Binary Mode)

When running nodes in binary mode (`--no-docker`), you can enable embedded authentication directly in `merod`. This provides JWT-based protection for all API endpoints without requiring Docker containers or external services.

#### Quick Start with Embedded Auth

```bash
# Start a node with embedded auth enabled
merobox run --no-docker --binary-path /path/to/merod --auth-mode embedded

# Start multiple nodes with embedded auth
merobox run --no-docker --binary-path /path/to/merod --count 2 --auth-mode embedded

# Run workflow with embedded auth (requires credentials for API calls)
merobox bootstrap run workflow.yml \
  --no-docker \
  --binary-path /path/to/merod \
  --auth-mode embedded \
  --auth-username admin \
  --auth-password password123
```

**Note**: When running workflows with `--auth-mode embedded`, you must provide `--auth-username` and `--auth-password`. These credentials are used to authenticate with each node before executing workflow steps. Users are automatically created on first authentication.

#### What Gets Enabled

When you use `--auth-mode embedded`:

1. **JWT Protection**: All API endpoints (`/jsonrpc`, `/admin-api/`, `/ws`) require valid JWT tokens
2. **Built-in Auth Service**: Authentication endpoints available at `/auth/*`
3. **User Management**: Username/password provider enabled by default
4. **Persistent Storage**: Auth data stored in `<node_home>/auth/` directory (RocksDB)

#### Authentication Endpoints

With embedded auth enabled, the following endpoints are available:

- **Auth Health**: `http://localhost:2528/auth/health` - Check auth service status
- **Auth Providers**: `http://localhost:2528/auth/providers` - List available auth providers
- **Auth Login UI**: `http://localhost:2528/auth/login` - Web-based login interface
- **Token Endpoint**: `http://localhost:2528/auth/token` - Programmatic token generation

#### Getting a JWT Token

**Option 1: Using the REST API**

```bash
curl -X POST http://localhost:2528/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "auth_method": "user_password",
    "public_key": "your-public-key",
    "client_name": "my-client",
    "timestamp": 1234567890,
    "permissions": [],
    "provider_data": {
      "username": "admin",
      "password": "password123"
    }
  }'
```

**Option 2: Using the Web UI**

Navigate to `http://localhost:2528/auth/login` in your browser to use the interactive authentication interface.

#### Using JWT Tokens

Once you have a token, include it in API requests:

```bash
curl http://localhost:2528/jsonrpc \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

#### Important Notes

- **Binary Mode Only**: `--auth-mode` only works with `--no-docker`. For Docker mode, use `--auth-service` instead.
- **Storage Location**: Auth data (users, tokens) is stored at `<node_home>/auth/` by default.
- **Default Provider**: Embedded auth uses username/password provider by default. Users are created automatically on first authentication.

#### Workflow Integration

Enable embedded auth in workflows:

```yaml
name: "Embedded Auth Workflow"
description: "Workflow with embedded authentication"

# Enable embedded auth (binary mode only)
auth_mode: embedded

nodes:
  count: 1
  base_port: 2428
  base_rpc_port: 2528
  chain_id: "testnet-1"

steps:
  - name: "Wait for startup"
    type: "wait"
    seconds: 5
```

**Note**: When using `auth_mode: embedded` in workflows, you must also use `--no-docker` flag and provide `--binary-path`.

---

## 🌐 Remote Nodes

Merobox supports connecting to remote Calimero nodes alongside local Docker/binary nodes. This enables running workflows against production or staging nodes with proper authentication.

### Quick Start with Remote Nodes

```bash
# Register a remote node
merobox remote register prod-node https://prod.example.com \
  --auth-method user_password \
  --username admin

# Login (stores token for future use)
merobox remote login prod-node

# Test connectivity
merobox remote test prod-node

# Check status
merobox remote status

# Run a workflow against the remote node
merobox bootstrap run workflow.yml
```

### Remote Node CLI Commands

#### `merobox remote register`

Register a remote node with a friendly name:

```bash
merobox remote register <name> <url> [OPTIONS]
```

**Options:**

- `--auth-method/-m`: Authentication method (`user_password`, `api_key`, `none`)
- `--username/-u`: Username for user_password auth
- `--description/-d`: Human-readable description

**Examples:**

```bash
# Register with user/password auth
merobox remote register prod https://prod.example.com -m user_password -u admin

# Register with API key auth
merobox remote register staging https://staging.example.com -m api_key

# Register without auth
merobox remote register local-remote https://localhost:8080 -m none
```

#### `merobox remote login`

Authenticate with a remote node and cache the token:

```bash
merobox remote login <url_or_name> [OPTIONS]
```

**Options:**

- `--username/-u`: Username (prompts if not provided)
- `--password/-p`: Password (prompts if not provided)
- `--api-key/-k`: API key for api_key auth method
- `--method/-m`: Auth method override

**Examples:**

```bash
# Login with prompts
merobox remote login prod-node

# Login with credentials
merobox remote login prod-node -u admin -p secret

# Login with API key
merobox remote login staging-node -k sk-your-api-key

# Login to unregistered URL
merobox remote login https://prod.example.com -u admin
```

#### `merobox remote logout`

Remove cached authentication:

```bash
merobox remote logout <url_or_name>
merobox remote logout --all  # Remove all cached tokens
```

#### `merobox remote status`

Display registered nodes and cached tokens:

```bash
merobox remote status
```

Shows:

- Registered remote nodes with URLs and auth methods
- Cached tokens with expiration status (valid/expired)

#### `merobox remote test`

Test connectivity and authentication:

```bash
merobox remote test <url_or_name> [OPTIONS]
```

**Options:**

- `--username/-u`: Username for testing
- `--password/-p`: Password for testing
- `--api-key/-k`: API key for testing

Tests performed:

1. Network connectivity
2. Auth requirement detection
3. Authentication (if required)
4. API access verification

#### `merobox remote list`

List all registered remote nodes:

```bash
merobox remote list
```

#### `merobox remote unregister`

Remove a registered remote node:

```bash
merobox remote unregister <name> [--remove-token]
```

**Options:**

- `--remove-token`: Also remove cached authentication token

### Remote Nodes in Workflows

Define remote nodes in your workflow YAML using the `remote_nodes` key:

```yaml
name: Remote Workflow Example
description: Workflow using remote nodes

# Remote nodes configuration
remote_nodes:
  prod-node:
    url: https://prod.example.com
    auth:
      method: user_password
      username: ${PROD_USERNAME}
      # Password from environment or cached token

  staging-node:
    url: https://staging.example.com
    auth:
      method: api_key
      key: ${STAGING_API_KEY}

  public-node:
    url: https://public.example.com
    auth:
      method: none

steps:
  - name: Install Application
    type: install_application
    node: prod-node # Reference remote node by name
    url: https://example.com/app.wasm
    outputs:
      app_id: applicationId

  - name: Create Context
    type: create_context
    node: prod-node
    application_id: "{{app_id}}"
    outputs:
      context_id: contextId
```

#### Environment Variable Expansion

Auth fields support `${ENV_VAR}` and `${ENV_VAR:-default}` syntax:

```yaml
remote_nodes:
  prod:
    url: https://prod.example.com
    auth:
      method: user_password
      username: ${MEROBOX_USERNAME:-admin}
      # Password read from MEROBOX_PASSWORD env var or cached token
```

#### Mixed Local and Remote Nodes

Workflows can use both local Docker nodes and remote nodes:

```yaml
name: Mixed Workflow

nodes:
  count: 2
  prefix: local-node

remote_nodes:
  production:
    url: https://prod.example.com
    auth:
      method: user_password
      username: admin

steps:
  - name: Setup on Local Node
    type: create_context
    node: local-node-1
    application_id: "{{app_id}}"

  - name: Verify on Remote
    type: call
    node: production
    context_id: "{{context_id}}"
    method: get_status
```

### Bootstrap CLI Options for Remote Nodes

Pass remote nodes directly via CLI without modifying workflow files:

```bash
# Add remote node for this run
merobox bootstrap run workflow.yml \
  --remote-node prod=https://prod.example.com

# Add authentication
merobox bootstrap run workflow.yml \
  --remote-node prod=https://prod.example.com \
  --remote-auth prod=admin:secret123

# Multiple remote nodes
merobox bootstrap run workflow.yml \
  --remote-node prod=https://prod.example.com \
  --remote-node staging=https://staging.example.com \
  --remote-auth prod=admin:pass1 \
  --remote-auth staging=apikey:sk-xxx

# Default API key for all nodes
merobox bootstrap run workflow.yml \
  --remote-node prod=https://prod.example.com \
  --api-key sk-default-key
```

**CLI Options:**

- `--remote-node NAME=URL` (repeatable): Register a remote node
- `--remote-auth NAME=AUTH` (repeatable): Set authentication
  - `name=user:password` for user_password auth
  - `name=apikey:KEY` for API key auth
- `--api-key KEY`: Default API key for nodes without explicit auth

### Authentication Methods

#### User/Password Authentication

Uses the `/auth/token` endpoint with username and password:

```yaml
auth:
  method: user_password
  username: admin
  # Password from: CLI flag, environment, or interactive prompt
```

**Credential Resolution Order:**

1. CLI flags (`--password`, `-p`)
2. Environment variable (`MEROBOX_PASSWORD`)
3. Interactive prompt

#### API Key Authentication

Uses a pre-generated API key:

```yaml
auth:
  method: api_key
  key: ${API_KEY} # From environment
```

#### No Authentication

For public nodes:

```yaml
auth:
  method: none
```

### Token Caching

Tokens are automatically cached to avoid repeated authentication:

- **Cache Location:** `~/.merobox/auth_cache/`
- **File Format:** `{node-slug}-{hash}.json`
- **Permissions:** Restrictive (0o600)

**How It Works:**

1. First authentication saves token to cache
2. Subsequent requests use cached token
3. Expired tokens are automatically refreshed
4. `calimero-client-py` handles refresh on 401 responses

**Token Lifecycle:**

```bash
# Initial login caches token
merobox remote login prod-node -u admin -p secret

# Subsequent commands use cached token (no prompts)
merobox bootstrap run workflow.yml

# Check token status
merobox remote status

# Force re-authentication
merobox remote logout prod-node
merobox remote login prod-node
```

### Environment Variables

| Variable           | Description                             |
| ------------------ | --------------------------------------- |
| `MEROBOX_USERNAME` | Default username for user_password auth |
| `MEROBOX_PASSWORD` | Default password for user_password auth |
| `MEROBOX_API_KEY`  | Default API key                         |

**Example:**

```bash
export MEROBOX_USERNAME=admin
export MEROBOX_PASSWORD=secret
merobox remote login prod-node  # Uses env vars, no prompts
```

### Installing Applications on Remote Nodes

Remote nodes require applications to be installed from URLs (not local paths):

```yaml
steps:
  - name: Install Application
    type: install_application
    node: remote-node
    # Use URL for remote nodes (not path)
    url: https://example.com/releases/app.wasm
    outputs:
      app_id: applicationId
```

**Note:** The `path` and `dev: true` options are only for local Docker/binary nodes where the file can be copied into the container or accessed from the host filesystem.

### Troubleshooting Remote Nodes

#### Connection Issues

```bash
# Test basic connectivity
merobox remote test prod-node

# Check if auth is required
curl -I https://prod.example.com/admin-api/health
```

#### Authentication Issues

```bash
# Clear cached token and re-authenticate
merobox remote logout prod-node
merobox remote login prod-node -u admin

# Check token status
merobox remote status
```

#### Token Expired

Tokens are automatically refreshed, but if issues persist:

```bash
# Force fresh authentication
merobox remote logout prod-node
merobox remote login prod-node
```

#### Environment Variable Issues

```bash
# Verify environment
echo $MEROBOX_USERNAME
echo $MEROBOX_PASSWORD

# Test with explicit credentials
merobox remote login prod-node -u admin -p secret
```

---

## 📖 Workflow Guide

### Overview

Merobox workflows are defined in YAML files and executed through the `bootstrap` command. Workflows can include multiple steps like installing applications, creating contexts, managing identities, and executing function calls.

### Workflow Structure

```yaml
name: "Sample Workflow"
nodes:
  - calimero-node-1
  - calimero-node-2

steps:
  - name: "Install Application"
    type: "install"
    node: "calimero-node-1"
    path: "./app.wasm"
    outputs:
      applicationId: "app_id"
```

### Step Types

#### Install Step

Installs WASM applications on Calimero nodes.

```yaml
- name: "Install App"
  type: "install"
  node: "calimero-node-1"
  path: "./application.wasm" # Local path
  # OR
  url: "https://example.com/app.wasm" # Remote URL
  dev: true # Development mode
  outputs:
    applicationId: "app_id"
```

#### Context Step

Creates blockchain contexts for applications.

```yaml
- name: "Create Context"
  type: "context"
  node: "calimero-node-1"
  application_id: "{{app_id}}"
  params:
    param1: "value1"
  outputs:
    contextId: "context_id"
    memberPublicKey: "member_key"
```

#### Identity Step

Generates cryptographic identities.

```yaml
- name: "Create Identity"
  type: "identity"
  node: "calimero-node-2"
  outputs:
    publicKey: "public_key"
```

#### Invite Step

Invites identities to join contexts.

```yaml
- name: "Invite Identity"
  type: "invite"
  node: "calimero-node-1"
  context_id: "{{context_id}}"
  grantee_id: "{{public_key}}"
  outputs:
    invitation: "invitation_data"
```

#### Join Step

Joins contexts using invitations.

```yaml
- name: "Join Context"
  type: "join"
  node: "calimero-node-2"
  context_id: "{{context_id}}"
  invitee_id: "{{public_key}}"
  invitation: "{{invitation_data}}"
```

#### Create Mesh Step

Creates a context and connects multiple nodes in a single step. It automatically creates a context on the specified node, generates identities for each target node, sends invitations, and joins all nodes to the context.

```yaml
- name: "Create Mesh"
  type: "create_mesh"
  context_node: "calimero-node-1"
  application_id: "{{app_id}}"
  nodes:
    - calimero-node-2
    - calimero-node-3
  capability: member # Optional, defaults to "member"
  outputs:
    context_id: contextId
    member_public_key: memberPublicKey
```

**What it does:**

- Creates a context on the specified `context_node`
- Creates identities on each node in the `nodes` list
- Invites each node from the context node
- Joins each node to the context

**Parameters:**

- `context_node`: Node where the context will be created
- `application_id`: Application ID to use for context creation
- `nodes`: List of node names to create identities on and join to the context
- `capability`: Optional capability for invitations (defaults to `"member"`).

**Exported variables:**

- `context_id`: The created context ID
- `member_public_key`: The context creator's public key
- `public_key_{node_name}`: Public key for each joined node (e.g., `public_key_calimero-node-2`)
- `public_key`: If only one node is joined, also exported as just `public_key`

#### Execute Step

Executes smart contract functions.

```yaml
- name: "Call Function"
  type: "call"
  node: "calimero-node-1"
  context_id: "{{context_id}}"
  method: "set"
  args:
    key: "hello"
    value: "world"
  executor_public_key: "{{member_key}}"
  outputs:
    result: "function_result"
```

**Negative Testing with Expected Failures:**

You can test error scenarios by setting `expected_failure: true`. When enabled, the step will continue even if the call fails, and error details are exported for assertions.

Both simple and complex output syntax are supported for error fields:

```yaml
- name: "Expected Failure - Invalid Method"
  type: "call"
  node: "calimero-node-1"
  context_id: "{{context_id}}"
  executor_public_key: "{{member_public_key}}"
  method: "invalid_method_that_does_not_exist"
  args: {}
  expected_failure: true
  outputs:
    # Simple string assignment
    error_code: error_code # JSON-RPC error code
    error_type: error_type # Error type (e.g., "FunctionCallError")
    error_message: error_message # Error message
    error: error # Full error object

    # Complex dict-based assignment (also supported)
    custom_error:
      field: error_type
    custom_error_code:
      field: error_code
      target: error_code_{node_name} # Optional: custom target with node name

- name: "Assert Error Occurred"
  type: assert
  statements:
    - "is_set({{error_type}})"
    - "equal({{error_type}}, FunctionCallError)"
    - "contains({{error_message}}, not found)"
    - "equal({{custom_error}}, FunctionCallError)"
```

**Error Handling:**

- **JSON-RPC Errors**: When a function call succeeds but returns a JSON-RPC error (e.g., invalid method, wrong arguments), error fields are extracted and exported.
- **Network/API Errors**: When the API call itself fails (e.g., invalid context ID, network issues), error information is captured and exported.
- **Unexpected Success**: If `expected_failure: true` but the call succeeds, a warning is shown and error fields are exported as `None`.

See `workflow-examples/workflow-negative-testing-example.yml` for comprehensive examples.

#### Wait Step

Adds delays between steps.

```yaml
- name: "Wait"
  type: "wait"
  seconds: 5
```

#### Wait for Sync Step

Waits for nodes to reach consensus by verifying they have the same root hash. This is more reliable than fixed wait times because it only proceeds once nodes are actually synchronized.

```yaml
- name: "Wait for Nodes to Sync"
  type: "wait_for_sync"
  context_id: "{{context_id}}"
  nodes:
    - calimero-node-1
    - calimero-node-2
  timeout: 30 # Max seconds to wait (default: 30)
  check_interval: 1 # Seconds between checks (default: 1.0)
  trigger_sync: true # Trigger sync before checking (default: true)
  outputs:
    root_hash: "synced_hash"
    elapsed_seconds: "sync_time"
```

**Key Features:**

- Verifies root hash consensus across specified nodes
- Fails workflow if nodes don't sync within timeout
- Provides detailed sync progress logging
- More reliable than fixed wait times
- Captures sync metrics (time, attempts, final hash)

**When to Use:**

- After state-changing operations (set, delete, update)
- After nodes join a context
- Before reading data from different nodes
- In critical workflows where consistency is required

#### Repeat Step

Executes steps multiple times.

```yaml
- name: "Repeat Operations"
  type: "repeat"
  count: 3
  steps:
    - name: "Set Value"
      type: "call"
      node: "calimero-node-1"
      context_id: "{{context_id}}"
      method: "set"
      args:
        key: "iteration_{{current_iteration}}"
        value: "value_{{current_iteration}}"
      executor_public_key: "{{member_key}}"
      outputs:
        result: "iteration_result"
    - name: "Wait"
      type: "wait"
      seconds: 2
  outputs:
    iteration: "current_iteration"
```

#### Fuzzy Test Step

Runs long-duration randomized load tests with weighted operation patterns and assertion-based validation.

```yaml
- name: "Fuzzy Load Test"
  type: "fuzzy_test"
  duration_minutes: 30 # Run for 30 minutes
  context_id: "{{context_id}}"
  success_threshold: 95.0 # Pass if 95%+ assertions succeed

  nodes:
    - name: calimero-node-1
      executor_key: "{{member_public_key}}"
    - name: calimero-node-2
      executor_key: "{{public_key_node2}}"

  operations:
    # Pattern 1: Write and verify (40% of operations)
    - name: "set_and_verify"
      weight: 40
      steps:
        # Resolved args auto-captured as {{fuzzy_key}}, {{fuzzy_value}}
        - type: call
          node: "{{random_node}}" # Random node selection
          method: set
          context_id: "{{context_id}}"
          executor_public_key: "{{random_executor}}"
          args:
            key: "test_{{random_int(1, 1000)}}" # Random generators
            value: "{{uuid}}_{{timestamp}}"

        - type: wait
          seconds: 1

        # Use auto-captured {{fuzzy_key}}
        - type: call
          node: "{{random_node}}"
          method: get
          context_id: "{{context_id}}"
          executor_public_key: "{{random_executor}}"
          args:
            key: "{{fuzzy_key}}"
          outputs:
            retrieved: result

        # Use auto-captured {{fuzzy_value}}
        - type: assert
          non_blocking: true # Failures don't stop the test
          statements:
            - statement: "contains({{retrieved}}, {{fuzzy_value}})"
              message: "Value should match"

    # Pattern 2: Cross-node propagation test (30% of operations)
    - name: "cross_node_sync"
      weight: 30
      steps:
        # Args auto-captured as {{fuzzy_key}}, {{fuzzy_value}}
        - type: call
          node: calimero-node-1
          method: set
          context_id: "{{context_id}}"
          executor_public_key: "{{member_public_key}}"
          args:
            key: "sync_{{random_int(1, 500)}}"
            value: "{{timestamp}}"

        - type: wait
          seconds: 2 # Wait for propagation

        # Use auto-captured {{fuzzy_key}}
        - type: call
          node: calimero-node-2
          method: get
          context_id: "{{context_id}}"
          executor_public_key: "{{public_key_node2}}"
          args:
            key: "{{fuzzy_key}}"
          outputs:
            synced_value: result

        # Use auto-captured {{fuzzy_value}}
        - type: assert
          non_blocking: true
          statements:
            - statement: "contains({{synced_value}}, {{fuzzy_value}})"
              message: "Data should propagate across nodes"

    # Pattern 3: Random reads (30% of operations)
    - name: "random_read"
      weight: 30
      steps:
        - type: call
          node: "{{random_node}}"
          method: get
          context_id: "{{context_id}}"
          executor_public_key: "{{random_executor}}"
          args:
            key: "test_{{random_int(1, 1000)}}"
```

**Random Value Generators:**

- `{{random_int(min, max)}}` - Random integer
- `{{random_string(length)}}` - Random alphanumeric string
- `{{random_float(min, max)}}` - Random float
- `{{random_choice([a, b, c])}}` - Random choice from list
- `{{timestamp}}` - Current Unix timestamp
- `{{uuid}}` - Random UUID
- `{{random_node}}` - Random node from nodes list
- `{{random_executor}}` - Random executor key

**Auto-Captured Arguments:**

When a `call` step executes, its resolved arguments are automatically captured with a `fuzzy_` prefix:

- `args.key` → `{{fuzzy_key}}`
- `args.value` → `{{fuzzy_value}}`
- `args.amount` → `{{fuzzy_amount}}`

This allows subsequent steps to reference the exact values used in previous calls for verification.

**Features:**

- **Application-agnostic**: Works with any smart contract
- **Weighted patterns**: Control operation frequency
- **Non-blocking assertions**: Track failures without stopping
- **Live progress**: Periodic summaries every 60 seconds
- **Detailed reporting**: Final report with pass rates and failure analysis

#### Upload Blob Step

Uploads files to blob storage and captures blob IDs for E2E testing.

```yaml
- name: "Upload File to Blob Storage"
  type: "upload_blob"
  node: "calimero-node-1"
  file_path: "res/kv_store.wasm" # Path to file on local filesystem
  context_id: "{{context_id}}" # Optional: associate with context
  outputs:
    blob_id: "wasm_blob_id" # Capture the blob ID
    size: "wasm_blob_size" # Capture the blob size

# Use the blob ID in contract calls
- name: "Register Blob in Contract"
  type: "call"
  node: "calimero-node-1"
  context_id: "{{context_id}}"
  method: "register_blob"
  args:
    blob_id: "{{wasm_blob_id}}" # Use real blob ID from upload
    size: "{{wasm_blob_size}}" # Use real size from upload
```

**Features:**

- Upload files from local filesystem to blob storage
- Capture `blob_id` and `size` for use in subsequent steps
- Optional context association via `context_id`
- Full binary file support
- Automatic error handling and validation

### Dynamic Variables

Workflows support dynamic variable substitution using `{{variable_name}}` syntax.

#### Variable Sources

- **Step Outputs**: Variables exported by previous steps
- **Workflow Context**: Global workflow variables
- **Environment**: System environment variables

#### Embedded Variables

Variables can be embedded within strings:

```yaml
args:
  key: "user_{{user_id}}_data_{{iteration}}"
```

#### Variable Resolution

- Variables are resolved at execution time
- Missing variables cause workflow failures
- Use `outputs` sections to export variables for later use

### Output Configuration

Each step can export variables for use in subsequent steps:

```yaml
outputs:
  variableName: "export_name" # Maps API response field to export name
```

### Example Workflows

- **Basic Workflow**: `workflow-examples/workflow-example.yml` - Complete example with dynamic variables
- **Remote Node Workflow**: `workflow-examples/workflow-remote-node-example.yml` - Using remote nodes with authentication
- **Negative Testing**: `workflow-examples/workflow-negative-testing-example.yml` - Testing error scenarios with expected failures
- **Assertions**: `workflow-examples/workflow-assert-example.yml` - Assertion and JSON assertion examples
- **Fuzzy Load Testing**: `workflow-examples/workflow-fuzzy-kv-store.yml` - Long-running load test with randomized operations

### Export variables from execute (call) steps

Call-like steps (type: `call`) return a JSON payload. You can export fields from that payload to named variables via the `outputs` mapping, and then reference those variables in subsequent steps using `{{variable_name}}`.

Example (from `workflow-execute-variables-example.yml`):

```yaml
- name: Execute Get
  type: call
  node: calimero-node-2
  context_id: "{{ctx_id}}"
  executor_public_key: "{{member_key}}"
  method: get
  args:
    key: example_key
  outputs:
    # Simple field access
    read_value: result

    # Nested field access with automatic JSON parsing
    # If result contains { "output": "value" }, this extracts "value"
    nested_value: result.output

    # Deep nesting also works: result.data.user.name.first
    deeply_nested: result.data.user.name

- name: Echo Exported Value
  type: script
  target: local
  inline: |
    echo "Exported value is: {{read_value}}"
    echo "Nested value is: {{nested_value}}"
```

**Syntax options:**

1. **Simple dotted path (recommended)**: Use dot notation to access nested fields. The system automatically parses JSON strings at each level.

   ```yaml
   outputs:
     my_value: result.output # Simple nested access
     deep_value: result.data.user.name # Deep nesting
     array_item: items.0.id # Array indexing
   ```

2. **Dict-based syntax (legacy, still supported)**: For backward compatibility, you can use the explicit dict form:
   ```yaml
   outputs:
     my_value:
       field: result # top-level field name to read from
       json: true # parse JSON if the field is a JSON string
       path: output # dotted path inside the parsed JSON
   ```

Notes:

- The `outputs` keys (e.g., `read_value`) become variables you can interpolate later as `{{read_value}}`.
- For more advanced mappings (including per-node variable names), see `workflow-custom-outputs-example.yml`.

### Running scripts in workflows (image, nodes, local) and passing args

The `script` step can execute a script in three ways:

- `target: image` runs the script inside a temporary container created from the node image (before nodes are started)
- `target: nodes` copies and runs the script inside each running Calimero node container
- `target: local` runs the script on your host machine via `/bin/sh`

You can also pass arguments and reference exported variables using placeholders. Arguments are resolved before execution.

Example:

```yaml
- name: Echo Exported Value
  type: script
  target: local # or "nodes" / "image"
  script: ./workflow-examples/scripts/echo-exported-value.sh
  args:
    - "{{read_value}}" # placeholder resolved from previous step outputs
```

Notes:

- The `script` field must be only the path to the script; pass parameters via the `args:` list.
- Placeholders in `args` are resolved using previously exported variables and workflow results.
- For container targets, the script is copied into the container and executed with `/bin/sh`.

### Assertion Steps

#### Assert (type: `assert`)

Statement-based assertions against exported variables and literals.

Supported forms:

- `is_set(A)` / `is_empty(A)`
- `contains(A, B)` / `not_contains(A, B)`
- `regex(A, PATTERN)`
- Comparisons: `A == B`, `A != B`, `A >= B`, `A > B`, `A <= B`, `A < B`
- Equality helpers: `equal(A, B)`, `equals(A, B)`, `not_equal(A, B)`, `not_equals(A, B)`

Placeholders like `{{var}}` are resolved before evaluation.

Example:

```yaml
- name: Assert exported variables
  type: assert
  statements:
    - "is_set({{context_id}})"
    - "{{count}} >= 1"
    - "contains({{get_result}}, 'hello')"
    - "regex({{value}}, '^abc')"
    - "equal({{a}}, {{b}})"
```

#### JSON Assert (type: `json_assert`)

Compare JSON-like values (Python dict/list or JSON strings).

Supported forms:

- `json_equal(A, B)` / `equal(A, B)`
- `json_subset(A, B)` / `subset(A, B)` (B must be subset of A)

Example:

```yaml
- name: Assert JSON equality of get_result
  type: json_assert
  statements:
    - "json_equal({{get_result}}, {'output': 'assert_value'})"
```

---

## 🎯 Local Blockchain Environments

Merobox supports two NEAR backends for workflows:

1. **Local NEAR Sandbox** (default) — A real, ephemeral NEAR blockchain running locally. No testnet tokens or RPC required; contracts are downloaded automatically.
2. **Relayer/Testnet** — Use `--enable-relayer` to connect nodes to the relayer and NEAR testnet instead of a local sandbox.

**Local Sandbox (default):**

- **Best for:** Full E2E testing and contract logic verification.
- **Behavior:** Executes actual WASM smart contracts and state transitions.

### Local NEAR Sandbox

Merobox runs workflows against a local ephemeral NEAR blockchain (Sandbox) by default, instead of the public Testnet.
This enables faster E2E testing without needing testnet tokens or RPC access.

#### Requirements

The compiled WebAssembly (`.wasm`) files for the Calimero context contracts are downloaded automatically. You can override with `--contracts-dir` if you have a local copy:

1. `calimero_context_config_near.wasm`
2. `calimero_context_proxy_near.wasm`

#### How to Run

Local sandbox is the default. Just run your workflow; contracts are downloaded automatically if needed:

```bash
merobox bootstrap run workflows/my-test.yml
```

To use the relayer/testnet instead, pass `--enable-relayer`.

**What happens during the run:**

1. **Sandbox Start**: Merobox downloads and starts `near-sandbox` locally on port 3030.
2. **Contract Deployment**: It creates a root account (`calimero.test.near`) and deploys the registry contracts.
3. **Node Configuration**: It generates funded NEAR accounts for every node in your workflow (e.g., `node-1.test.near`).
4. **Config Injection**: It overrides the node's `config.toml` to point to the local sandbox RPC (`http://host.docker.internal:3030` for Docker nodes).
5. **Cleanup**: The sandbox and all chain data are destroyed when the workflow finishes.

---

## 🔧 API Reference

### Command Overview

```bash
merobox [OPTIONS] COMMAND [ARGS]...
```

### Global Options

- `--version`: Show version and exit
- `--help`: Show help message and exit

### Core Commands

#### `merobox run`

Start Calimero nodes.

```bash
merobox run [OPTIONS]
```

**Options:**

- `--count INTEGER`: Number of nodes to start (default: 1)
- `--prefix TEXT`: Node name prefix (default: "calimero-node")
- `--restart`: Restart existing nodes
- `--image TEXT`: Custom Docker image to use
- `--force-pull`: Force pull Docker image even if it exists locally
- `--no-docker`: Run nodes as native processes instead of Docker containers
- `--binary-path PATH`: Path to merod binary (required when using `--no-docker`)
- `--auth-service`: Enable authentication service with Traefik proxy (Docker mode only)
- `--auth-image TEXT`: Custom Docker image for the auth service (default: ghcr.io/calimero-network/mero-auth:edge)
- `--auth-mode [embedded|proxy]`: Authentication mode for merod (binary mode only). `embedded` enables built-in auth with JWT protection on all endpoints. Default is `proxy` (no embedded auth).
- `--log-level TEXT`: Set the RUST_LOG level for Calimero nodes (default: debug). Supports complex patterns like 'info,module::path=debug'
- `--rust-backtrace TEXT`: Set the RUST_BACKTRACE level for Calimero nodes (default: 0).
- `--help`: Show help message

#### `merobox stop`

Stop Calimero nodes.

```bash
merobox stop [OPTIONS]
```

**Options:**

- `--all`: Stop all running nodes and auth service stack
- `--auth-service`: Stop auth service stack only (Traefik + Auth)
- `--prefix TEXT`: Stop nodes with specific prefix
- `--help`: Show help message

#### `merobox list`

List running Calimero nodes.

```bash
merobox list [OPTIONS]
```

**Options:**

- `--help`: Show help message

#### `merobox health`

Check health status of nodes.

```bash
merobox health [OPTIONS]
```

**Options:**

- `--help`: Show help message

#### `merobox logs`

View node logs.

```bash
merobox logs [OPTIONS] NODE_NAME
```

**Options:**

- `--follow`: Follow log output
- `--help`: Show help message

#### `merobox bootstrap`

Execute workflows and validate configurations.

```bash
merobox bootstrap [OPTIONS] COMMAND [ARGS]...
```

**Subcommands:**

- `run <config_file>`: Execute a workflow
- `validate <config_file>`: Validate workflow configuration
- `create-sample`: Create a sample workflow file

**Run Command Options:**

- `--auth-service`: Enable authentication service with Traefik proxy (Docker mode only)
- `--auth-image TEXT`: Custom Docker image for the auth service (d