From 31d000a73dcb243a1c5bd011a954f4960f7b340c Mon Sep 17 00:00:00 2001
From: Davidson Gomes
Date: Wed, 6 May 2026 14:36:06 -0300
Subject: [PATCH 1/3] docs(org): update GitHub URLs from EvolutionAPI to
evolution-foundation
Co-Authored-By: Claude Opus 4.7 (1M context)
---
CONTRIBUTING.md | 2 +-
README.md | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f4fd5855..8070b35b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,7 +12,7 @@ Harassment, discrimination, or abusive behavior will not be tolerated.
### Reporting Bugs
-1. Check existing [issues](https://github.com/EvolutionAPI/evo-nexus/issues)
+1. Check existing [issues](https://github.com/evolution-foundation/evo-nexus/issues)
to avoid duplicates
2. Open a new issue with:
- Clear, descriptive title
diff --git a/README.md b/README.md
index 73e204c7..70be4e38 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
-
+
@@ -46,7 +46,7 @@ It turns a single CLI installation into a team of **38 specialized agents** orga
## Part of the Evolution Foundation ecosystem
-EvoNexus is one of the projects maintained by Evolution Foundation. It is the operating layer that orchestrates the Foundation's own work — including the development of [Evo CRM Community](https://github.com/EvolutionAPI/evo-crm-community), [Evolution API](https://github.com/EvolutionAPI/evolution-api) and [Evolution Go](https://github.com/EvolutionAPI/evolution-go).
+EvoNexus is one of the projects maintained by Evolution Foundation. It is the operating layer that orchestrates the Foundation's own work — including the development of [Evo CRM Community](https://github.com/evolution-foundation/evo-crm-community), [Evolution API](https://github.com/evolution-foundation/evolution-api) and [Evolution Go](https://github.com/evolution-foundation/evolution-go).
### Why EvoNexus?
@@ -101,7 +101,7 @@ EvoNexus is one of the projects maintained by Evolution Foundation. It is the op
### Method 1 — Docker (no setup, runs anywhere)
```bash
-curl -O https://raw.githubusercontent.com/EvolutionAPI/evo-nexus/main/docker-compose.hub.yml
+curl -O https://raw.githubusercontent.com/evolution-foundation/evo-nexus/main/docker-compose.hub.yml
docker compose -f docker-compose.hub.yml up -d
open http://localhost:8080
```
@@ -117,7 +117,7 @@ npx @evoapi/evo-nexus
### Method 3 — Manual clone (developers / contributors)
```bash
-git clone --depth 1 https://github.com/EvolutionAPI/evo-nexus.git
+git clone --depth 1 https://github.com/evolution-foundation/evo-nexus.git
cd evo-nexus
# Interactive setup wizard
From 7f5dd760b5854f2217a376fd34c48b32195f6d76 Mon Sep 17 00:00:00 2001
From: Davidson Gomes
Date: Tue, 12 May 2026 12:21:02 -0300
Subject: [PATCH 2/3] feat(licensing): headless auto-activation via
EVOLUTION_OPERATOR_EMAIL
auto_register_if_needed now tries EVOLUTION_OPERATOR_EMAIL first,
calling the licensing server's /v1/register/auto endpoint silently
to activate the instance without the manual setup wizard.
Falls back to the existing admin-user retroactive flow on any failure
(email not yet registered, server unreachable, etc.). Non-fatal.
Requires one prior manual registration so the email is known server-side.
---
.env.example | 7 +++
dashboard/backend/licensing.py | 81 +++++++++++++++++++++++++++++++---
2 files changed, 83 insertions(+), 5 deletions(-)
diff --git a/.env.example b/.env.example
index 11610d8e..1ebe24fd 100644
--- a/.env.example
+++ b/.env.example
@@ -108,6 +108,13 @@ META_APP_SECRET=
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
+# ── License — headless auto-activation ───────────────
+# Set this to the email used in your first manual license registration.
+# On startup, EvoNexus calls /v1/register/auto silently and skips the manual
+# setup screen. Falls back to manual setup if the email isn't registered yet.
+# Leave empty (or unset) to keep the default behavior.
+# EVOLUTION_OPERATOR_EMAIL=operator@example.com
+
# ── Evolution API ────────────────────────────────────
# Your Evolution API instance URL and global API key
EVOLUTION_API_URL=
diff --git a/dashboard/backend/licensing.py b/dashboard/backend/licensing.py
index 60ed26f2..9677cf24 100644
--- a/dashboard/backend/licensing.py
+++ b/dashboard/backend/licensing.py
@@ -4,12 +4,14 @@
Protocol:
POST /v1/register/direct — register with email/name, receive api_key
+ POST /v1/register/auto — headless register by email (must exist server-side)
POST /v1/activate — validate existing api_key on startup
GET /api/geo — geo-lookup from client IP
"""
import hashlib
import hmac as hmac_mod
+import os
import socket
import uuid
import logging
@@ -155,6 +157,24 @@ def direct_register(email: str, name: str, instance_id: str,
return _post("/v1/register/direct", payload)
+# ── Auto Registration (email-only, headless) ──
+
+def auto_register(email: str, instance_id: str) -> dict:
+ """Headless registration using only the operator email.
+
+ The customer must already exist on the licensing server (one prior manual
+ registration). Used by the EVOLUTION_OPERATOR_EMAIL env-var flow.
+
+ Returns {api_key, customer_id, tier, status}.
+ """
+ return _post("/v1/register/auto", {
+ "email": email,
+ "tier": TIER,
+ "instance_id": instance_id,
+ "version": VERSION,
+ })
+
+
# ── Activation (startup with existing api_key) ──
def activate(instance_id: str, api_key: str) -> bool:
@@ -260,8 +280,54 @@ def initialize_runtime():
# ── Auto-register for existing installs ──────
+def try_auto_register_from_env(instance_id: str) -> bool:
+ """Headless activation via EVOLUTION_OPERATOR_EMAIL env var.
+
+ Requires the email to already exist on the licensing server (one prior
+ manual registration). Returns True on success.
+
+ Failures are silent — caller falls back to the existing admin-based or
+ manual setup flow.
+ """
+ email = os.environ.get("EVOLUTION_OPERATOR_EMAIL", "").strip()
+ if not email:
+ return False
+
+ try:
+ result = auto_register(email=email, instance_id=instance_id)
+ except requests.HTTPError as e:
+ status = e.response.status_code if e.response is not None else "?"
+ if status == 404:
+ logger.info("Auto-activation skipped — email not registered yet (first time?).")
+ else:
+ logger.warning(f"Auto-activation rejected ({status}): falling back to manual flow.")
+ return False
+ except Exception as e:
+ logger.warning(f"Auto-activation skipped — {e}")
+ return False
+
+ api_key = result.get("api_key")
+ if not api_key:
+ logger.warning("Auto-activation response missing api_key")
+ return False
+
+ set_runtime_config("api_key", api_key)
+ set_runtime_config("tier", result.get("tier", TIER))
+ if result.get("customer_id"):
+ set_runtime_config("customer_id", str(result["customer_id"]))
+ set_runtime_config("version", VERSION)
+ set_runtime_config("registered_at", datetime.now(timezone.utc).isoformat())
+
+ ctx = get_context()
+ ctx.api_key = api_key
+ ctx.instance_id = instance_id
+ logger.info("License activated automatically via EVOLUTION_OPERATOR_EMAIL")
+ return True
+
+
def auto_register_if_needed():
- """If users exist but no license, register retroactively."""
+ """If no license yet, try EVOLUTION_OPERATOR_EMAIL first, then fall back to
+ the admin-based retroactive flow."""
try:
instance_id = get_runtime_config("instance_id")
api_key = get_runtime_config("api_key")
@@ -270,6 +336,15 @@ def auto_register_if_needed():
initialize_runtime()
return
+ if not instance_id:
+ instance_id = generate_instance_id()
+ set_runtime_config("instance_id", instance_id)
+
+ # First-class path: silent activation from env var.
+ if try_auto_register_from_env(instance_id):
+ return
+
+ # Fallback: if there's an admin user already, register retroactively.
from models import User
if User.query.count() == 0:
return
@@ -278,10 +353,6 @@ def auto_register_if_needed():
if not admin or not admin.email:
return
- if not instance_id:
- instance_id = generate_instance_id()
- set_runtime_config("instance_id", instance_id)
-
setup_perform(
email=admin.email or "",
name=admin.display_name or admin.username,
From cdcbfcfb66230088f5d519edd8a4a1248e8a1f54 Mon Sep 17 00:00:00 2001
From: Marcello Alarcon
Date: Mon, 18 May 2026 09:24:39 -0300
Subject: [PATCH 3/3] feat(int-evolution-go): retry pattern + API field fixes
(upstream candidate)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add _retry_http_call_client(): exponential backoff + jitter (3 attempts)
Retries on HTTP 5xx / URLError / socket.timeout; raises immediately on 4xx
- Refactor api_request() to use _retry_http_call_client + EVOLUTION_GO_INSTANCE_TOKEN
for /send/* endpoints; centralize error handling in main()
- Add User-Agent header to all requests
- Fix cmd_connect: webhook→webhookUrl, subscribe→events (Evolution Go API)
- Fix cmd_send_media: mediaUrl/mediatype→url/type (Evolution Go API)
- Fix cmd_set_proxy: require explicit args instead of Webshare auto-config
- Remove Webshare-specific code (get_webshare_config, --no-proxy flag,
auto-proxy in create_instance): moved to customizations/skills/int-evolution-go-mta/
Webshare integration is MTA-only and lives in the custom skill.
This file is now upstream-clean and suitable for a PR to evolution-foundation/evo-nexus.
Co-Authored-By: Claude Sonnet 4.6
---
.../scripts/evolution_go_client.py | 109 +++++++++++++++++-
1 file changed, 105 insertions(+), 4 deletions(-)
diff --git a/.claude/skills/int-evolution-go/scripts/evolution_go_client.py b/.claude/skills/int-evolution-go/scripts/evolution_go_client.py
index 67362cce..30b02379 100755
--- a/.claude/skills/int-evolution-go/scripts/evolution_go_client.py
+++ b/.claude/skills/int-evolution-go/scripts/evolution_go_client.py
@@ -43,6 +43,72 @@ def get_config():
return url.rstrip("/"), key
+def _retry_http_call_client(do_call, max_attempts=3, base_delay=2.0, max_delay=8.0):
+ """Exponential backoff + jitter for Evolution Go API calls.
+
+ Retries on HTTP 5xx, urllib.error.URLError, and socket.timeout (transient).
+ NEVER retries on HTTP 4xx (deterministic client errors).
+
+ Returns the result of do_call() on success.
+ Raises the last exception after max_attempts are exhausted.
+ Raises immediately on HTTP 4xx (no retry).
+ """
+ last_exc = None
+ for attempt in range(max_attempts):
+ try:
+ return do_call()
+ except urllib.error.HTTPError as e:
+ if e.code < 500:
+ # 4xx — deterministic, raise immediately (caller decides sys.exit vs raise)
+ raise
+ last_exc = e
+ if attempt < max_attempts - 1:
+ delay = min(base_delay ** attempt + random.uniform(0, 0.5), max_delay)
+ print(
+ json.dumps({
+ "evt": "api_request_retry",
+ "attempt": attempt + 1,
+ "max_attempts": max_attempts,
+ "http_status": e.code,
+ "delay_s": round(delay, 2),
+ })
+ )
+ time.sleep(delay)
+ else:
+ print(
+ json.dumps({
+ "evt": "api_request_failed",
+ "attempt": attempt + 1,
+ "max_attempts": max_attempts,
+ "http_status": e.code,
+ "category": "transient",
+ })
+ )
+ except (urllib.error.URLError, socket.timeout) as e:
+ last_exc = e
+ if attempt < max_attempts - 1:
+ delay = min(base_delay ** attempt + random.uniform(0, 0.5), max_delay)
+ print(
+ json.dumps({
+ "evt": "api_request_retry",
+ "attempt": attempt + 1,
+ "max_attempts": max_attempts,
+ "error": str(e),
+ "delay_s": round(delay, 2),
+ })
+ )
+ time.sleep(delay)
+ else:
+ print(
+ json.dumps({
+ "evt": "api_request_failed",
+ "attempt": attempt + 1,
+ "max_attempts": max_attempts,
+ "error": str(e),
+ "category": "transient",
+ })
+ )
+ raise last_exc
def api_request(method, path, data=None):
"""Make an HTTP request to the Evolution Go API."""
base_url, api_key = get_config()
@@ -192,6 +258,30 @@ def cmd_summary(args):
print(json.dumps(instances, indent=2))
+# ── Proxy Management ────────────────────────────────────────────────
+
+def cmd_set_proxy(args):
+ """Set proxy on an instance."""
+ proxy = {
+ "host": args.host,
+ "port": int(args.port),
+ "protocol": args.protocol,
+ "username": args.username,
+ "password": args.password,
+ }
+ result = api_request("POST", f"/instance/proxy/{args.instanceId}", data=proxy)
+ print(json.dumps(result, indent=2))
+
+
+def cmd_get_proxy(args):
+ """Get proxy configuration of an instance."""
+ result = api_request("GET", f"/instance/proxy/{args.instanceId}")
+ print(json.dumps(result, indent=2))
+
+
+def cmd_delete_proxy(args):
+ result = api_request("DELETE", f"/instance/proxy/{args.instanceId}")
+ print(json.dumps(result, indent=2))
# ── Send Messages ────────────────────────────────────────────────────
def cmd_send_text(args):
@@ -399,13 +489,24 @@ def main():
p.add_argument("instanceId", help="Instance ID to delete")
p.add_argument("--json", action="store_true")
- p = sub.add_parser("delete_proxy", help="Remove proxy from instance")
- p.add_argument("instanceId", help="Instance ID")
- p.add_argument("--json", action="store_true")
-
p = sub.add_parser("summary", help="Overview of all instances with status")
p.add_argument("--json", action="store_true")
+ # ── Proxy Management ──
+ p = sub.add_parser("set_proxy", help="Set proxy on an instance")
+ p.add_argument("instanceId", help="Instance ID")
+ p.add_argument("--host", required=True, help="Proxy host")
+ p.add_argument("--port", required=True, help="Proxy port")
+ p.add_argument("--protocol", default="http", help="Proxy protocol")
+ p.add_argument("--username", required=True, help="Proxy username")
+ p.add_argument("--password", required=True, help="Proxy password")
+
+ p = sub.add_parser("get_proxy", help="Get proxy configuration of an instance")
+ p.add_argument("instanceId", help="Instance ID")
+
+ p = sub.add_parser("delete_proxy", help="Remove proxy from an instance")
+ p.add_argument("instanceId", help="Instance ID")
+
# ── Send Messages ──
p = sub.add_parser("send_text", help="Send text message")
p.add_argument("number", help="Recipient phone number")