From 5f561a523c9bd3edef0c0f00ed380c861a1ce692 Mon Sep 17 00:00:00 2001 From: Lumina Developer Date: Sat, 16 May 2026 15:15:26 +0700 Subject: [PATCH 1/2] Add safe-competition-trading-agent plugin --- .../.claude-plugin/plugin.json | 18 + .../safe-competition-trading-agent/.gitignore | 11 + skills/safe-competition-trading-agent/LICENSE | 21 + .../safe-competition-trading-agent/README.md | 94 ++++ .../safe-competition-trading-agent/SKILL.md | 242 ++++++++++ .../safe-competition-trading-agent/SUMMARY.md | 39 ++ .../plugin.yaml | 31 ++ .../pyproject.toml | 25 ++ .../safe-competition-trading-agent/setup.py | 3 + .../__init__.py | 1 + .../__main__.py | 3 + .../candidate_selector.py | 110 +++++ .../src/safe_competition_trading_agent/cli.py | 110 +++++ .../safe_competition_trading_agent/models.py | 137 ++++++ .../safe_competition_trading_agent/planner.py | 413 ++++++++++++++++++ .../safe_competition_trading_agent/render.py | 16 + .../risk_bridge.py | 69 +++ .../tests/conftest.py | 5 + .../tests/test_candidate_selector.py | 23 + .../tests/test_planner.py | 86 ++++ .../tests/test_safe_competition_cli.py | 74 ++++ 21 files changed, 1531 insertions(+) create mode 100644 skills/safe-competition-trading-agent/.claude-plugin/plugin.json create mode 100644 skills/safe-competition-trading-agent/.gitignore create mode 100644 skills/safe-competition-trading-agent/LICENSE create mode 100644 skills/safe-competition-trading-agent/README.md create mode 100644 skills/safe-competition-trading-agent/SKILL.md create mode 100644 skills/safe-competition-trading-agent/SUMMARY.md create mode 100644 skills/safe-competition-trading-agent/plugin.yaml create mode 100644 skills/safe-competition-trading-agent/pyproject.toml create mode 100644 skills/safe-competition-trading-agent/setup.py create mode 100644 skills/safe-competition-trading-agent/src/safe_competition_trading_agent/__init__.py create mode 100644 skills/safe-competition-trading-agent/src/safe_competition_trading_agent/__main__.py create mode 100644 skills/safe-competition-trading-agent/src/safe_competition_trading_agent/candidate_selector.py create mode 100644 skills/safe-competition-trading-agent/src/safe_competition_trading_agent/cli.py create mode 100644 skills/safe-competition-trading-agent/src/safe_competition_trading_agent/models.py create mode 100644 skills/safe-competition-trading-agent/src/safe_competition_trading_agent/planner.py create mode 100644 skills/safe-competition-trading-agent/src/safe_competition_trading_agent/render.py create mode 100644 skills/safe-competition-trading-agent/src/safe_competition_trading_agent/risk_bridge.py create mode 100644 skills/safe-competition-trading-agent/tests/conftest.py create mode 100644 skills/safe-competition-trading-agent/tests/test_candidate_selector.py create mode 100644 skills/safe-competition-trading-agent/tests/test_planner.py create mode 100644 skills/safe-competition-trading-agent/tests/test_safe_competition_cli.py diff --git a/skills/safe-competition-trading-agent/.claude-plugin/plugin.json b/skills/safe-competition-trading-agent/.claude-plugin/plugin.json new file mode 100644 index 000000000..3bbc2aca7 --- /dev/null +++ b/skills/safe-competition-trading-agent/.claude-plugin/plugin.json @@ -0,0 +1,18 @@ +{ + "name": "safe-competition-trading-agent", + "description": "Competition-aware Agentic Wallet trading skill with risk-gated execution", + "version": "1.0.0", + "author": { + "name": "Safe Competition Trading Agent Contributors" + }, + "license": "MIT", + "keywords": [ + "trading", + "competition", + "agentic-wallet", + "xlayer", + "solana", + "security", + "risk" + ] +} diff --git a/skills/safe-competition-trading-agent/.gitignore b/skills/safe-competition-trading-agent/.gitignore new file mode 100644 index 000000000..463b07e10 --- /dev/null +++ b/skills/safe-competition-trading-agent/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.coverage +dist/ +build/ +*.egg-info/ +.env +.venv/ +venv/ diff --git a/skills/safe-competition-trading-agent/LICENSE b/skills/safe-competition-trading-agent/LICENSE new file mode 100644 index 000000000..0e900f5a4 --- /dev/null +++ b/skills/safe-competition-trading-agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Safe Competition Trading Agent Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/safe-competition-trading-agent/README.md b/skills/safe-competition-trading-agent/README.md new file mode 100644 index 000000000..254b70c68 --- /dev/null +++ b/skills/safe-competition-trading-agent/README.md @@ -0,0 +1,94 @@ +# Safe Competition Trading Agent + +Competition-aware Agentic Wallet trading skill with risk-gated execution. + +This plugin is designed as the main strategy skill for OKX Agentic Wallet trading competitions. It uses onchainOS as the primary data and trading layer, then composes a safe workflow with `agent-workflow-composer` and blocks unsafe trades with `agent-risk-firewall`. + +## Why This Exists + +Agentic trading competitions need more than a simple swap command. A useful agent must know: + +- whether the competition is active; +- whether the wallet has joined; +- which chains count for the competition; +- whether the selected pair is an eligible real-token trade; +- whether quote, slippage, price impact, and transaction context are safe enough; +- whether the user has explicitly confirmed execution. + +`safe-competition-trading-agent` turns those checks into an ordered strategy workflow. + +## What It Does + +| Area | Behavior | +| --- | --- | +| Competition preflight | Plans `onchainos competition list/detail/user-status` before trading. | +| Candidate selection | Scores supplied onchainOS token candidates and prefers liquid, lower-risk real tokens. | +| Quote and tx context | Requires onchainOS swap quote and unsigned transaction preparation before firewall. | +| Workflow validation | Uses a deterministic plan that places composer and firewall before execution. | +| Risk gate | Requires `agent-risk-firewall` verdict before execution. | +| Execution safety | Defaults to dry-run; live execution requires confirmation and an allow/warn verdict. | + +## Commands + +```powershell +safe-competition-trading-agent plan --input request.json --format json +safe-competition-trading-agent dry-run --input request.json --format json +safe-competition-trading-agent execute --input request.json --format json +safe-competition-trading-agent validate --input plan.json --format json +safe-competition-trading-agent template --name competition-safe-swap +safe-competition-trading-agent self-test +``` + +## Example Request + +```json +{ + "intent": "Dry-run a safe competition trade with a 10 USD budget.", + "chain": "xlayer", + "goal": "safe-volume", + "budgetUsd": "10", + "tokenIn": {"symbol": "USDC"}, + "executionMode": "dry-run" +} +``` + +## Live Execution Boundary + +The CLI does not sign or broadcast transactions by itself. It returns a guarded execution command only when: + +- `executionMode` is `confirm-before-execute`; +- `confirmed` is `true`; +- risk verdict is `allow`, or `warn` with confirmation; +- the workflow has passed all safety gates. + +The host agent still executes through onchainOS Agentic Wallet. + +## Testing + +From the repository root: + +```powershell +$env:PYTHONDONTWRITEBYTECODE = "1" +python -m pytest .\skills\safe-competition-trading-agent\tests -q -p no:cacheprovider +& "$env:USERPROFILE\.local\bin\plugin-store.exe" lint .\skills\safe-competition-trading-agent +``` + +Expected results: + +```text +tests passed +Plugin 'safe-competition-trading-agent' passed all checks +``` + +## Safety Notes + +- Do not execute before competition detail and user-status are known. +- Do not execute before quote and unsigned transaction context exist. +- Do not execute before `agent-risk-firewall check`. +- Do not execute on a `block` verdict. +- Do not use `--force`. +- Do not request, store, or export private keys, seed phrases, or mnemonics. + +## License + +MIT diff --git a/skills/safe-competition-trading-agent/SKILL.md b/skills/safe-competition-trading-agent/SKILL.md new file mode 100644 index 000000000..fa310363e --- /dev/null +++ b/skills/safe-competition-trading-agent/SKILL.md @@ -0,0 +1,242 @@ +--- +name: safe-competition-trading-agent +description: "Competition-aware Agentic Wallet trading skill that uses onchainOS data, risk firewall checks, and explicit user confirmation before execution" +version: "1.0.0" +author: "Safe Competition Trading Agent Contributors" +tags: + - trading + - competition + - agentic-wallet + - xlayer + - solana + - security + - risk +--- + +# Safe Competition Trading Agent + +## Overview + +Use this skill when the user wants an Agentic Wallet agent to plan or execute a safer competition trade. This is the main judged strategy skill: it uses onchainOS as the primary data source and trading tool, then uses `agent-workflow-composer` and `agent-risk-firewall` as safety layers. + +The skill is competition-aware. It checks whether the wallet is registered, whether the competition is active, which chains count, whether the candidate is a real token trade, and whether the quote/unsigned transaction passes a risk gate before the agent asks the user to confirm execution. + +Default mode is `dry-run`. Live execution is allowed only after: + +1. onchainOS competition detail and user-status are known. +2. onchainOS quote and unsigned transaction context are prepared. +3. `agent-risk-firewall check` returns `allow`, or returns `warn` and the user explicitly confirms. +4. The user has explicitly requested execution. + +## Trigger Phrases + +Use this skill for requests like: + +- "trade for competition" +- "safe competition trade" +- "agentic trading competition" +- "find a safe token for competition" +- "trade 10 dollars for contest" +- "optimize my competition rank" +- "increase my competition volume" +- "dry-run competition trade" +- "run a safe competition swap" +- "join competition and trade" +- "giao dich cho cuoc thi" +- "trade cho competition" +- "tim token an toan de thi" +- "toi uu thu hang cuoc thi" +- "tang volume cuoc thi" +- "giao dich 10 do cho competition" +- "kiem tra trade nay co tinh diem khong" + +Do not use this skill for generic wallet balance, normal token portfolio checks, exporting wallets, or unrelated DeFi positions. Use the corresponding wallet, portfolio, DeFi, or DEX skills for those. + +## Required onchainOS Role + +This skill must use onchainOS as the primary source of truth: + +| Purpose | Required command family | +|---|---| +| Wallet session | `onchainos wallet status` | +| Competition discovery and rules | `onchainos competition list`, `onchainos competition detail` | +| Registration status | `onchainos competition user-status` | +| Token discovery and metadata | `onchainos token ...` or the active onchainOS token skill | +| Quote | `onchainos swap quote` | +| Unsigned transaction context | onchainOS swap transaction preparation command; prepare only, do not sign or broadcast | +| Live execution after gates | `onchainos swap execute` | + +Use `agent-workflow-composer` to validate workflow order and `agent-risk-firewall` to produce the final risk verdict. + +## Competition Invariants + +Apply these rules when building the plan: + +- Every active competition currently counts Solana plus the backend primary chain. If detail says X Layer, treat both X Layer and Solana as supported until backend exposes a full multi-chain field. +- Trades on either supported chain count toward the same standing. +- Keep internal ids such as `activityId`, `chainIndex`, and `accountId` in tool context only. Do not render them in user-visible messages. +- Identify competitions to the user by name, not by numeric id. +- `myRankInfo.userTotal = 0` can mean the user has not hit the threshold or the backend has not updated yet; it does not mean the chain is unsupported. + +## CLI Commands + +```bash +safe-competition-trading-agent plan --input request.json --format json +safe-competition-trading-agent dry-run --input request.json --format json +safe-competition-trading-agent execute --input request.json --format json +safe-competition-trading-agent validate --input plan.json --format json +safe-competition-trading-agent template --name competition-safe-swap +safe-competition-trading-agent self-test +``` + +The CLI never signs or broadcasts. It creates plans, dry-run decisions, guarded execution commands, and local validation reports. + +## Input Contract + +```json +{ + "intent": "Dry-run a safe competition trade with a 10 USD budget.", + "chain": "xlayer", + "goal": "safe-volume", + "budgetUsd": "10", + "tokenIn": {"symbol": "USDC"}, + "executionMode": "dry-run", + "confirmed": false, + "competition": { + "activityName": "Selected Agentic Trading competition", + "active": true, + "joined": true, + "chainName": "X Layer", + "supportedChains": ["xlayer", "solana"], + "eligibleTokenTradeRequired": true + }, + "candidates": [ + { + "symbol": "MEME", + "address": "0x...", + "liquidityUsd": "150000", + "volume24hUsd": "75000", + "riskLevel": "LOW" + } + ], + "quote": { + "expectedOut": "12345", + "slippagePct": 1, + "priceImpactPct": 1 + }, + "riskVerdict": { + "verdict": "allow", + "reasons": [] + } +} +``` + +Supported values: + +- `chain`: `xlayer`, `solana` +- `goal`: `safe-volume`, `rank-optimizer`, `eligible-token`, `custom` +- `executionMode`: `dry-run`, `confirm-before-execute` + +## Execution Flow + +Follow this exact order: + +1. Parse user intent: chain, budget, goal, dry-run vs execution. +2. Run `onchainos wallet status`. +3. Run `onchainos competition list --status 0`. +4. Select the competition by user intent or ask the user to choose by name. +5. Run `onchainos competition detail --activity-id `. +6. Run `onchainos competition user-status --activity-id --evm-wallet --sol-wallet `. +7. If the user has not joined, warn that the trade may not count. Ask whether to join before trading. +8. Query onchainOS token data for candidate tokens on the selected chain. +9. Exclude stable/native-only candidates for competition mode unless the user explicitly asks for a non-eligible dry-run. +10. Run `onchainos swap quote`. +11. Prepare unsigned swap transaction context through onchainOS only after the user has approved the dry-run preparation step. This preparation step is not live execution and must not sign or broadcast. +12. Run `agent-workflow-composer plan` with `workflowType: "competition-trade"`. +13. Run `agent-risk-firewall check` with `policyProfile: "competition"`. +14. Apply the verdict: + - `block`: stop. Do not ask for signature. + - `warn`: show reasons and ask for explicit confirmation. + - `allow`: continue only if the user already requested execution. +15. If execution is confirmed, run `onchainos swap execute`. +16. Show tx result, risk decision id, and next step. + +## Output Format + +For user-facing output, use this structure: + +```md +## Competition Trade Plan + +Competition: +Chain: +Mode: +Budget: + +## Candidate + +Token: +Reason: +- + +## Quote + +Route: +Estimated out: +Slippage: +Price impact: + +## Risk Verdict + +Verdict: +Risk score: <0-100> +Reasons: +- + +## Next Step + + +``` + +Do not show `activityId`, `accountId`, private keys, seed phrases, mnemonics, raw secrets, or internal-only identifiers. + +## Safety Rules + +- Default to `dry-run`. +- Never execute when `agent-risk-firewall` returns `block`. +- Never execute on `warn` without explicit user confirmation. +- Never execute if `competition_user_status` is missing. +- Never execute if quote or unsigned transaction context is missing. +- Never use `--force`. +- Never export or request private keys, seed phrases, or mnemonics. +- Never claim that a trade will definitely win a competition. + +## Error Handling + +| Situation | Response | +|---|---| +| Wallet not logged in | Ask user to run or approve wallet login before trading. | +| No active competition | Stop and show that no active competition was found. | +| User not joined | Warn and ask whether to join before trading. | +| No token candidate | Query onchainOS token/signal data; if still none, stop. | +| Quote unavailable | Retry once; if still unavailable, stop. | +| Firewall unavailable | Treat as `warn` or stop in strict review contexts; do not execute blindly. | +| Firewall block | Cancel trade. | + +## Example Agent Behavior + +User: "Trade 10 dollars for a safe competition token on X Layer." + +Agent should: + +1. Explain it will dry-run first. +2. Fetch wallet and competition context through onchainOS. +3. Select a real token candidate from onchainOS data. +4. Quote and prepare unsigned tx context through onchainOS. +5. Run workflow composer and risk firewall. +6. Show the structured output. +7. Ask for explicit confirmation only if the trade is not blocked. + +## Disclaimer + +This skill is a trading guardrail and strategy workflow, not a guarantee of profit or safety. On-chain data, competition rules, rankings, quotes, and simulations can change or be stale. Trading can cause loss of funds. diff --git a/skills/safe-competition-trading-agent/SUMMARY.md b/skills/safe-competition-trading-agent/SUMMARY.md new file mode 100644 index 000000000..c3faf6741 --- /dev/null +++ b/skills/safe-competition-trading-agent/SUMMARY.md @@ -0,0 +1,39 @@ +# Safe Competition Trading Agent + +## Overview + +Safe Competition Trading Agent is a competition-aware Agentic Wallet strategy skill. It uses onchainOS competition, wallet, token, quote, and swap commands as the main data and trading layer, then applies workflow and risk gates before any live execution. + +Core operations: + +- Plan a competition trade from a user intent, chain, budget, and goal +- Fetch competition detail and user registration status through onchainOS +- Select safer real-token candidates from onchainOS token context +- Prepare quote and unsigned transaction context through onchainOS +- Validate workflow order with `agent-workflow-composer` +- Enforce risk verdicts with `agent-risk-firewall` +- Require explicit user confirmation before live execution + +Tags: `trading` `competition` `agentic-wallet` `xlayer` `solana` `security` `risk` + +## Prerequisites + +- Python 3.8+ +- Agentic Wallet login for live execution +- onchainOS CLI/skills installed +- Recommended supporting plugins: + - `okx-agentic-wallet` + - `okx-growth-competition` + - `okx-dex-token` + - `okx-dex-swap` + - `agent-workflow-composer` + - `agent-risk-firewall` + +## Quick Start + +1. **Build a plan** with `safe-competition-trading-agent plan --input request.json --format json`. +2. **Dry-run first** with competition, quote, and risk context. +3. **Review risk verdict** from `agent-risk-firewall`. +4. **Execute only after explicit confirmation** and only when the firewall allows or warns with confirmed consent. + +The plugin does not sign, broadcast, export wallets, or handle private keys by itself. diff --git a/skills/safe-competition-trading-agent/plugin.yaml b/skills/safe-competition-trading-agent/plugin.yaml new file mode 100644 index 000000000..e9f7b06a1 --- /dev/null +++ b/skills/safe-competition-trading-agent/plugin.yaml @@ -0,0 +1,31 @@ +schema_version: 1 +name: safe-competition-trading-agent +version: "1.0.0" +description: "Competition-aware Agentic Wallet trading skill with risk-gated execution" +author: + name: "Safe Competition Trading Agent Contributors" + github: "maixuancanh" +license: MIT +category: trading-strategy +tags: + - trading + - competition + - agentic-wallet + - xlayer + - solana + - security + - risk + +components: + skill: + dir: "." + +build: + lang: python + source_repo: maixuancanh/agent-risk-firewall + source_commit: "dac401ca47dd1214965463b7b40551dada80ebaa" + binary_name: safe-competition-trading-agent + main: "src/safe_competition_trading_agent/cli.py" + +api_calls: + - web3.okx.com diff --git a/skills/safe-competition-trading-agent/pyproject.toml b/skills/safe-competition-trading-agent/pyproject.toml new file mode 100644 index 000000000..5d2e41d75 --- /dev/null +++ b/skills/safe-competition-trading-agent/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "safe-competition-trading-agent" +version = "1.0.0" +description = "Competition-aware Agentic Wallet trading skill with risk-gated execution" +requires-python = ">=3.8" +license = { text = "MIT" } +authors = [ + { name = "Safe Competition Trading Agent Contributors" } +] +keywords = ["trading", "competition", "agentic-wallet", "xlayer", "solana", "security", "risk"] +dependencies = [] + +[project.scripts] +safe-competition-trading-agent = "safe_competition_trading_agent.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] diff --git a/skills/safe-competition-trading-agent/setup.py b/skills/safe-competition-trading-agent/setup.py new file mode 100644 index 000000000..606849326 --- /dev/null +++ b/skills/safe-competition-trading-agent/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/__init__.py b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/__init__.py new file mode 100644 index 000000000..5becc17c0 --- /dev/null +++ b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/__main__.py b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/__main__.py new file mode 100644 index 000000000..eb53e2f31 --- /dev/null +++ b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +raise SystemExit(main()) diff --git a/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/candidate_selector.py b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/candidate_selector.py new file mode 100644 index 000000000..e21681a3d --- /dev/null +++ b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/candidate_selector.py @@ -0,0 +1,110 @@ +from decimal import Decimal +from typing import Any, Dict, List, Optional, Tuple + +from .models import optional_decimal + + +STABLE_SYMBOLS = {"USDC", "USDT", "DAI", "USDE", "USDS", "PYUSD", "FDUSD", "TUSD", "FRAX", "USD0"} +NATIVE_SYMBOLS = {"OKB", "WOKB", "SOL", "WSOL", "ETH", "WETH", "BNB", "WBNB", "MATIC", "WMATIC"} +BAD_TAGS = {"honeypot", "scam", "rug", "rugpull", "phishing", "blacklist", "malicious"} + + +def select_candidate(request: Dict[str, Any]) -> Dict[str, Any]: + explicit = request.get("tokenOut") + if explicit: + candidate = dict(explicit) + score, reasons = score_candidate(candidate) + candidate.setdefault("source", "user-request") + candidate["selectionScore"] = score + candidate["selectionReasons"] = reasons + return candidate + + candidates = request.get("candidates") or [] + if not candidates: + token = {"symbol": "", "source": "onchainos-token-discovery-required"} + token["selectionScore"] = 0 + token["selectionReasons"] = ["No token candidate supplied; agent must query onchainOS token/signal data."] + return token + + scored: List[Tuple[int, Dict[str, Any], List[str]]] = [] + for candidate in candidates: + score, reasons = score_candidate(candidate) + scored.append((score, candidate, reasons)) + scored.sort(key=lambda item: (-item[0], str(item[1].get("symbol") or item[1].get("address") or ""))) + best_score, best, reasons = scored[0] + output = dict(best) + output["selectionScore"] = best_score + output["selectionReasons"] = reasons + return output + + +def score_candidate(candidate: Dict[str, Any]) -> Tuple[int, List[str]]: + score = 50 + reasons: List[str] = [] + symbol = str(candidate.get("symbol") or "").upper() + tags = _lower_set(candidate.get("tags") or candidate.get("tokenTags") or candidate.get("riskTags") or []) + risk_level = str(candidate.get("riskLevel") or candidate.get("riskControlLevel") or "").upper() + + if symbol in STABLE_SYMBOLS or symbol in NATIVE_SYMBOLS: + score -= 35 + reasons.append("Candidate is stable/native; competition mode prefers real token trades.") + if risk_level in ("CRITICAL", "HIGH"): + score -= 45 + reasons.append("Candidate has high or critical risk metadata.") + elif risk_level in ("LOW", "SAFE"): + score += 10 + reasons.append("Candidate has low risk metadata.") + if tags & BAD_TAGS: + score -= 60 + reasons.append("Candidate has critical risk tags.") + + liquidity = _decimal(candidate, ["liquidityUsd", "liquidity", "liquidityUSD"]) + if liquidity is not None: + if liquidity >= Decimal("100000"): + score += 15 + reasons.append("Liquidity is healthy.") + elif liquidity < Decimal("25000"): + score -= 20 + reasons.append("Liquidity is low for competition trading.") + + volume = _decimal(candidate, ["volume24hUsd", "volumeUsd", "volume24h"]) + if volume is not None: + if volume >= Decimal("50000"): + score += 10 + reasons.append("Recent trading activity is meaningful.") + elif volume < Decimal("5000"): + score -= 10 + reasons.append("Recent trading activity is thin.") + + holder_concentration = _decimal(candidate, ["top10HolderPercent", "topHolderPercent"]) + if holder_concentration is not None and holder_concentration >= Decimal("70"): + score -= 15 + reasons.append("Holder concentration is elevated.") + + score = max(0, min(100, score)) + if not reasons: + reasons.append("Candidate selected from supplied onchainOS context.") + return score, reasons + + +def is_real_competition_token(token: Optional[Dict[str, Any]]) -> bool: + if not token: + return False + symbol = str(token.get("symbol") or "").upper() + return bool(symbol) and symbol not in STABLE_SYMBOLS and symbol not in NATIVE_SYMBOLS + + +def _decimal(candidate: Dict[str, Any], keys: List[str]) -> Optional[Decimal]: + for key in keys: + value = optional_decimal(candidate.get(key)) + if value is not None: + return value + return None + + +def _lower_set(values: Any) -> set: + if isinstance(values, str): + values = [values] + if not isinstance(values, list): + return set() + return {str(value).strip().lower() for value in values} diff --git a/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/cli.py b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/cli.py new file mode 100644 index 000000000..10d46de63 --- /dev/null +++ b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/cli.py @@ -0,0 +1,110 @@ +import argparse +from typing import Optional + +from .models import InputError, read_input +from .planner import build_plan, dry_run, execute, template, validate_plan +from .render import dumps_json, error_payload + + +def main(argv: Optional[list] = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + if args.command == "plan": + return _with_input(args, build_plan, success_code=0) + if args.command == "dry-run": + return _with_input(args, dry_run, success_code=0) + if args.command == "execute": + return _with_input(args, execute, success_code=0) + if args.command == "validate": + return _cmd_validate(args) + if args.command == "template": + payload = template(args.name) + print(dumps_json(payload)) + return 1 if "error" in payload else 0 + if args.command == "self-test": + return _cmd_self_test() + + parser.print_help() + return 2 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="safe-competition-trading-agent", + description="Plan and guard OKX Agentic Wallet competition trades.", + ) + subparsers = parser.add_subparsers(dest="command") + + for command, help_text in ( + ("plan", "Build a competition-aware trading plan."), + ("dry-run", "Evaluate a dry-run trade context without execution."), + ("execute", "Return a guarded execution command only after confirmation and risk allow/warn."), + ("validate", "Validate a generated plan."), + ): + sub = subparsers.add_parser(command, help=help_text) + sub.add_argument("--input", required=True, help="Path to input JSON, or '-' for stdin.") + sub.add_argument("--format", default="json", choices=["json"], help="Output format.") + + template_cmd = subparsers.add_parser("template", help="Print a starter request template.") + template_cmd.add_argument("--name", default="competition-safe-swap", help="competition-safe-swap or rank-optimizer.") + template_cmd.add_argument("--format", default="json", choices=["json"], help="Output format.") + + subparsers.add_parser("self-test", help="Run local strategy checks.") + return parser + + +def _with_input(args: argparse.Namespace, handler, success_code: int) -> int: + try: + payload = read_input(args.input) + print(dumps_json(handler(payload))) + return success_code + except OSError as exc: + print(dumps_json(error_payload("INPUT_READ_FAILED", "Could not read input.", [str(exc)]))) + return 2 + except InputError as exc: + print(dumps_json(error_payload(exc.code, exc.message, exc.details))) + return 2 + + +def _cmd_validate(args: argparse.Namespace) -> int: + try: + payload = read_input(args.input) + plan = payload if "steps" in payload else build_plan(payload) + result = validate_plan(plan) + print(dumps_json(result)) + return 0 if result.get("ok") else 1 + except OSError as exc: + print(dumps_json(error_payload("INPUT_READ_FAILED", "Could not read input.", [str(exc)]))) + return 2 + except InputError as exc: + print(dumps_json(error_payload(exc.code, exc.message, exc.details))) + return 2 + + +def _cmd_self_test() -> int: + payload = template("competition-safe-swap") + plan = build_plan(payload) + dry = dry_run( + dict( + payload, + tokenOut={"symbol": "MEME", "liquidityUsd": "150000", "riskLevel": "LOW"}, + competition={"active": True, "joined": True, "chainName": "X Layer", "supportedChains": ["xlayer", "solana"]}, + quote={"slippagePct": 1, "priceImpactPct": 1}, + riskVerdict={"verdict": "allow", "reasons": []}, + ) + ) + validation = validate_plan(plan) + checks = [ + {"case": "build-plan", "ok": plan["validation"]["ok"] is True}, + {"case": "validate-plan", "ok": validation["ok"] is True}, + {"case": "dry-run", "ok": dry["status"] == "ready"}, + {"case": "no-execute-in-dry-run", "ok": "execute_after_confirmation" not in [step["id"] for step in plan["steps"]]}, + ] + passed = all(item["ok"] for item in checks) + print(dumps_json({"status": "pass" if passed else "fail", "checks": checks})) + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/models.py b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/models.py new file mode 100644 index 000000000..bb62729c3 --- /dev/null +++ b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/models.py @@ -0,0 +1,137 @@ +import json +import sys +from decimal import Decimal, InvalidOperation +from typing import Any, Dict, List, Optional + + +SUPPORTED_CHAINS = {"xlayer", "solana"} +SUPPORTED_EXECUTION_MODES = {"dry-run", "confirm-before-execute"} +SUPPORTED_GOALS = {"safe-volume", "rank-optimizer", "eligible-token", "custom"} + + +class InputError(Exception): + def __init__(self, code: str, message: str, details: Optional[List[str]] = None): + super().__init__(message) + self.code = code + self.message = message + self.details = details or [] + + +def read_input(path: str) -> Dict[str, Any]: + if path == "-": + raw = sys.stdin.read() + else: + with open(path, "r", encoding="utf-8") as handle: + raw = handle.read() + try: + payload = json.loads(raw) + except json.JSONDecodeError as exc: + raise InputError("INVALID_JSON", "Input is not valid JSON.", [str(exc)]) + if not isinstance(payload, dict): + raise InputError("INVALID_INPUT", "Input JSON must be an object.") + return payload + + +def normalize_request(payload: Dict[str, Any]) -> Dict[str, Any]: + intent = str(payload.get("intent") or "").strip() + chain = normalize_chain(payload.get("chain") or "xlayer") + execution_mode = _choice(payload.get("executionMode"), "dry-run") + goal = _choice(payload.get("goal"), "safe-volume") + budget_usd = optional_decimal(payload.get("budgetUsd", payload.get("amountUsd"))) + + errors: List[str] = [] + if not intent: + errors.append("Missing required field: intent.") + if chain not in SUPPORTED_CHAINS: + errors.append("Unsupported chain. Use xlayer or solana.") + if execution_mode not in SUPPORTED_EXECUTION_MODES: + errors.append("Unsupported executionMode. Use dry-run or confirm-before-execute.") + if goal not in SUPPORTED_GOALS: + errors.append("Unsupported goal. Use safe-volume, rank-optimizer, eligible-token, or custom.") + if budget_usd is not None and budget_usd <= 0: + errors.append("budgetUsd must be greater than zero when provided.") + if errors: + raise InputError("INVALID_INPUT", "Input failed required-field validation.", errors) + + plugins = payload.get("plugins") if isinstance(payload.get("plugins"), dict) else {} + return { + "intent": intent, + "chain": chain, + "goal": goal, + "budgetUsd": budget_usd, + "tokenIn": normalize_token(payload.get("tokenIn")) or {"symbol": "USDC"}, + "tokenOut": normalize_token(payload.get("tokenOut")), + "executionMode": execution_mode, + "confirmed": bool(payload.get("confirmed")), + "wallet": _object(payload.get("wallet")), + "competition": _object(payload.get("competition")), + "candidates": _list_of_objects(payload.get("candidates")), + "quote": _object(payload.get("quote")), + "unsignedTx": _object(payload.get("unsignedTx") or payload.get("tx")), + "riskVerdict": _object(payload.get("riskVerdict")), + "constraints": _object(payload.get("constraints")), + "plugins": { + "wallet": plugins.get("wallet") or "okx-agentic-wallet", + "competition": plugins.get("competition") or "okx-growth-competition", + "token": plugins.get("token") or "okx-dex-token", + "signal": plugins.get("signal") or "okx-dex-signal", + "swap": plugins.get("swap") or "okx-dex-swap", + "composer": plugins.get("composer") or "agent-workflow-composer", + "risk": plugins.get("risk") or "agent-risk-firewall", + }, + } + + +def normalize_chain(value: Any) -> str: + text = str(value or "").strip().lower().replace("_", "-") + aliases = { + "x-layer": "xlayer", + "x layer": "xlayer", + "196": "xlayer", + "sol": "solana", + "501": "solana", + } + return aliases.get(text, text) + + +def normalize_token(value: Any) -> Optional[Dict[str, Any]]: + if value is None: + return None + if isinstance(value, str): + symbol = value.strip() + return {"symbol": symbol} if symbol else None + if isinstance(value, dict): + token = dict(value) + if "symbol" in token and isinstance(token["symbol"], str): + token["symbol"] = token["symbol"].strip() + return token + return {"raw": value} + + +def optional_decimal(value: Any) -> Optional[Decimal]: + if value is None or value == "": + return None + try: + return Decimal(str(value)) + except (InvalidOperation, ValueError): + return None + + +def decimal_to_str(value: Any) -> Any: + if isinstance(value, Decimal): + return str(value) + return value + + +def _choice(value: Any, default: str) -> str: + return str(value or default).strip().lower().replace("_", "-") + + +def _object(value: Any) -> Dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _list_of_objects(value: Any) -> List[Dict[str, Any]]: + if not isinstance(value, list): + return [] + return [item for item in value if isinstance(item, dict)] diff --git a/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/planner.py b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/planner.py new file mode 100644 index 000000000..4c4075a44 --- /dev/null +++ b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/planner.py @@ -0,0 +1,413 @@ +import hashlib +import json +from typing import Any, Dict, Iterable, List + +from .candidate_selector import is_real_competition_token, select_candidate +from .models import decimal_to_str, normalize_request +from .risk_bridge import build_firewall_input, risk_gate + + +AGENT_VERSION = "1.0.0" + + +def template(name: str = "competition-safe-swap") -> Dict[str, Any]: + normalized = str(name or "competition-safe-swap").strip().lower().replace("_", "-") + if normalized in ("competition-safe-swap", "safe-competition-trade"): + return { + "intent": "Dry-run a safe competition trade with a 10 USD budget.", + "chain": "xlayer", + "goal": "safe-volume", + "budgetUsd": "10", + "tokenIn": {"symbol": "USDC"}, + "executionMode": "dry-run", + "plugins": { + "wallet": "okx-agentic-wallet", + "competition": "okx-growth-competition", + "token": "okx-dex-token", + "signal": "okx-dex-signal", + "swap": "okx-dex-swap", + "composer": "agent-workflow-composer", + "risk": "agent-risk-firewall", + }, + } + if normalized == "rank-optimizer": + payload = template("competition-safe-swap") + payload["intent"] = "Find a safer eligible trade that may improve my Agentic Trading competition rank." + payload["goal"] = "rank-optimizer" + payload["budgetUsd"] = "25" + return payload + return { + "error": { + "code": "UNKNOWN_TEMPLATE", + "message": "Unknown template. Use competition-safe-swap or rank-optimizer.", + } + } + + +def build_plan(payload: Dict[str, Any]) -> Dict[str, Any]: + request = normalize_request(payload) + candidate = select_candidate(request) + steps = _steps(request, candidate) + plan = { + "strategyId": "scta_" + _sha256(_stable_json(request))[:16], + "agentVersion": AGENT_VERSION, + "status": "planned", + "intent": request["intent"], + "chain": request["chain"], + "goal": request["goal"], + "budgetUsd": decimal_to_str(request.get("budgetUsd")), + "executionMode": request["executionMode"], + "selectedCandidate": candidate, + "requiredPlugins": _required_plugins(request), + "steps": steps, + "safetyGates": _safety_gates(), + "outputContract": _output_contract(), + "runbook": _runbook(request), + } + plan["validation"] = validate_plan(plan) + return plan + + +def dry_run(payload: Dict[str, Any]) -> Dict[str, Any]: + request = normalize_request(payload) + candidate = select_candidate(request) + plan = build_plan(payload) + firewall_input = build_firewall_input(request, candidate) + readiness = _dry_run_readiness(request, candidate) + return { + "strategyId": plan["strategyId"], + "agentVersion": AGENT_VERSION, + "status": readiness["status"], + "mode": "dry-run", + "selectedCandidate": candidate, + "competitionContextStatus": _competition_context_status(request), + "quoteStatus": "ready" if request.get("quote") else "needs_onchainos_quote", + "firewallInput": firewall_input, + "riskGate": risk_gate(request), + "nextAction": readiness["nextAction"], + "plan": plan, + } + + +def execute(payload: Dict[str, Any]) -> Dict[str, Any]: + request = normalize_request(payload) + candidate = select_candidate(request) + gate = risk_gate(request) + command = _execute_command(request, candidate) + if request["executionMode"] != "confirm-before-execute": + return { + "status": "blocked", + "reason": "executionMode must be confirm-before-execute for live execution.", + "riskGate": gate, + "executionCommand": command, + } + if not request.get("confirmed"): + return { + "status": "blocked", + "reason": "Explicit user confirmation is required before execution.", + "riskGate": gate, + "executionCommand": command, + } + if not gate["okToExecute"]: + return { + "status": "blocked", + "reason": "Risk gate does not allow execution.", + "riskGate": gate, + "executionCommand": command, + } + return { + "status": "ready-to-execute", + "riskGate": gate, + "executionCommand": command, + "mustRunAfter": ["agent-risk-firewall check", "explicit user confirmation"], + "note": "CLI does not sign or broadcast; host agent must execute with onchainOS Agentic Wallet.", + } + + +def validate_plan(plan: Dict[str, Any]) -> Dict[str, Any]: + errors: List[Dict[str, str]] = [] + warnings: List[Dict[str, str]] = [] + steps = plan.get("steps") if isinstance(plan.get("steps"), list) else [] + ids = [str(step.get("id")) for step in steps if isinstance(step, dict)] + + required = [ + "wallet_preflight", + "competition_detail", + "competition_user_status", + "token_discovery", + "quote_swap", + "prepare_unsigned_swap", + "workflow_composer_plan", + "risk_firewall_check", + "user_confirmation_gate", + ] + for step_id in required: + if step_id not in ids: + errors.append(_issue("MISSING_STEP", "Missing required step: " + step_id)) + + if "risk_firewall_check" in ids and "prepare_unsigned_swap" in ids and ids.index("prepare_unsigned_swap") > ids.index("risk_firewall_check"): + errors.append(_issue("FIREWALL_BEFORE_TX_CONTEXT", "Firewall must run after quote and unsigned transaction context.")) + if "risk_firewall_check" in ids and "competition_user_status" in ids and ids.index("competition_user_status") > ids.index("risk_firewall_check"): + errors.append(_issue("FIREWALL_BEFORE_COMPETITION_STATUS", "Firewall must run after competition user-status.")) + if "execute_after_confirmation" in ids: + if ids.index("risk_firewall_check") > ids.index("execute_after_confirmation"): + errors.append(_issue("EXECUTE_BEFORE_FIREWALL", "Execution appears before risk firewall.")) + if ids.index("user_confirmation_gate") > ids.index("execute_after_confirmation"): + errors.append(_issue("EXECUTE_BEFORE_CONFIRMATION", "Execution appears before user confirmation gate.")) + execute_step = steps[ids.index("execute_after_confirmation")] + if execute_step.get("requiresConfirmation") is not True: + errors.append(_issue("EXECUTE_WITHOUT_CONFIRMATION", "Execution step must require confirmation.")) + + for step in steps: + command = str(step.get("command") or "") + if "--force" in command: + errors.append(_issue("FORCE_NOT_ALLOWED", "Generated commands must not include --force.")) + if not _uses_onchainos(steps): + errors.append(_issue("ONCHAINOS_NOT_USED", "Strategy must use onchainOS as data source and trading tool.")) + if plan.get("executionMode") == "dry-run" and "execute_after_confirmation" in ids: + errors.append(_issue("DRY_RUN_HAS_EXECUTION", "Dry-run plan must not include execution step.")) + if plan.get("executionMode") == "confirm-before-execute": + warnings.append(_issue("LIVE_EXECUTION_GUARDED", "Live execution is available only after firewall and explicit confirmation.")) + + return {"ok": not errors, "errors": errors, "warnings": warnings} + + +def _steps(request: Dict[str, Any], candidate: Dict[str, Any]) -> List[Dict[str, Any]]: + plugins = request["plugins"] + steps: List[Dict[str, Any]] = [ + { + "id": "parse_intent", + "title": "Parse competition trading intent", + "plugin": "safe-competition-trading-agent", + "mode": "local", + "produces": ["chain", "budgetUsd", "goal", "executionMode"], + }, + { + "id": "wallet_preflight", + "title": "Check Agentic Wallet session and addresses", + "plugin": plugins["wallet"], + "mode": "read-only", + "command": "onchainos wallet status", + "produces": ["walletStatus", "evmWallet", "solWallet"], + }, + { + "id": "competition_discovery", + "title": "Discover active Agentic Trading competitions", + "plugin": plugins["competition"], + "mode": "read-only", + "command": "onchainos competition list --status 0", + "produces": ["availableCompetitions"], + "mustNot": ["show internal activityId values to the user"], + }, + { + "id": "competition_detail", + "title": "Fetch selected competition rules", + "plugin": plugins["competition"], + "mode": "read-only", + "command": "onchainos competition detail --activity-id ", + "requires": ["selectedCompetition"], + "produces": ["competitionDetail"], + "mustNot": ["show internal activityId values to the user"], + }, + { + "id": "competition_user_status", + "title": "Check registration status for selected competition", + "plugin": plugins["competition"], + "mode": "read-only", + "command": "onchainos competition user-status --activity-id --evm-wallet --sol-wallet ", + "requires": ["competitionDetail", "evmWallet", "solWallet"], + "produces": ["competitionUserStatus"], + }, + { + "id": "token_discovery", + "title": "Find eligible real-token candidates from onchainOS data", + "plugin": plugins["token"], + "mode": "read-only", + "command": "onchainos token hot-tokens --chain ", + "requires": ["competitionDetail"], + "produces": ["tokenCandidates"], + }, + { + "id": "candidate_selection", + "title": "Select safer competition token candidate", + "plugin": "safe-competition-trading-agent", + "mode": "local", + "requires": ["tokenCandidates"], + "produces": ["selectedCandidate"], + "selectedCandidate": candidate, + }, + { + "id": "quote_swap", + "title": "Get onchainOS swap quote", + "plugin": plugins["swap"], + "mode": "read-only", + "command": "onchainos swap quote --from --to --readable-amount --chain ", + "requires": ["selectedCandidate", "walletStatus"], + "produces": ["quote"], + }, + { + "id": "prepare_unsigned_swap", + "title": "Prepare unsigned transaction context only", + "plugin": plugins["swap"], + "mode": "pre-execution", + "command": "onchainos swap swap --from --to --readable-amount --chain --wallet ", + "requires": ["quote", "walletAddress"], + "produces": ["unsignedTransaction"], + "mustNot": ["sign", "broadcast", "execute"], + }, + { + "id": "workflow_composer_plan", + "title": "Validate plugin workflow order", + "plugin": plugins["composer"], + "mode": "local", + "command": "agent-workflow-composer plan --input --format json", + "requires": ["competitionDetail", "quote", "unsignedTransaction"], + "produces": ["workflowPlan"], + }, + { + "id": "risk_firewall_check", + "title": "Run Agent Risk Firewall competition policy", + "plugin": plugins["risk"], + "mode": "risk-gate", + "command": "agent-risk-firewall check --input --format json", + "requires": ["competitionDetail", "competitionUserStatus", "quote", "unsignedTransaction"], + "produces": ["riskVerdict", "riskScore", "riskReasons", "firewallAudit"], + }, + { + "id": "user_confirmation_gate", + "title": "Require explicit user confirmation", + "plugin": "safe-competition-trading-agent", + "mode": "confirmation", + "requires": ["riskVerdict"], + "agentInstruction": "If block, stop. If warn, show reasons and require explicit user confirmation. If allow, continue only if user already requested execution.", + "produces": ["explicitUserConfirmation"], + }, + ] + if request["executionMode"] == "confirm-before-execute": + steps.append( + { + "id": "execute_after_confirmation", + "title": "Execute guarded swap through Agentic Wallet", + "plugin": plugins["swap"], + "mode": "execution", + "command": _execute_command(request, candidate), + "requires": ["riskVerdict", "explicitUserConfirmation"], + "requiresConfirmation": True, + "condition": "riskVerdict is allow, or riskVerdict is warn and explicitUserConfirmation is true", + "mustNot": ["--force", "execute after block verdict"], + } + ) + return steps + + +def _dry_run_readiness(request: Dict[str, Any], candidate: Dict[str, Any]) -> Dict[str, str]: + if not request.get("competition"): + return {"status": "needs_competition_context", "nextAction": "Run onchainOS competition detail and user-status."} + if not is_real_competition_token(candidate): + return {"status": "needs_token_candidate", "nextAction": "Query onchainOS token data and select a real token candidate."} + if not request.get("quote"): + return {"status": "needs_quote", "nextAction": "Run onchainOS swap quote and prepare unsigned tx context."} + gate = risk_gate(request) + if gate["state"] == "risk_verdict_missing": + return {"status": "needs_firewall", "nextAction": "Run agent-risk-firewall check with competition policy."} + if gate["state"] == "blocked": + return {"status": "blocked", "nextAction": "Cancel or revise the trade."} + if gate["state"] == "needs_explicit_confirmation": + return {"status": "needs_confirmation", "nextAction": "Show risk reasons and ask user to confirm explicitly."} + return {"status": "ready", "nextAction": "Use confirm-before-execute only if the user explicitly wants live execution."} + + +def _competition_context_status(request: Dict[str, Any]) -> str: + competition = request.get("competition") or {} + if not competition: + return "missing" + if competition.get("active") is False: + return "inactive" + if competition.get("joined") is False: + return "not_joined" + return "ready" + + +def _execute_command(request: Dict[str, Any], candidate: Dict[str, Any]) -> str: + token_in = (request.get("tokenIn") or {}).get("symbol") or "" + token_out = candidate.get("address") or candidate.get("symbol") or "" + amount = decimal_to_str(request.get("budgetUsd")) or "" + return ( + "onchainos swap execute --from {from_token} --to {to_token} " + "--readable-amount {amount} --chain {chain} --wallet " + ).format(from_token=token_in, to_token=token_out, amount=amount, chain=request["chain"]) + + +def _required_plugins(request: Dict[str, Any]) -> List[Dict[str, str]]: + purposes = { + "wallet": "Agentic Wallet session and addresses", + "competition": "competition list, detail, join status, rank context", + "token": "onchain token discovery and token metadata", + "swap": "onchainOS quote, unsigned transaction, and guarded execution", + "composer": "workflow order validation", + "risk": "pre-sign risk verdict and audit trail", + } + return [ + {"name": request["plugins"][key], "purpose": purposes[key]} + for key in ("wallet", "competition", "token", "swap", "composer", "risk") + ] + + +def _safety_gates() -> List[Dict[str, Any]]: + return [ + { + "id": "competition-eligibility", + "rule": "Do not execute until competition detail and user-status are known.", + }, + { + "id": "risk-firewall", + "rule": "Do not execute on block; require explicit confirmation on warn.", + }, + { + "id": "user-confirmation", + "rule": "Live execution requires confirmed=true and a prior user instruction to execute.", + }, + ] + + +def _output_contract() -> Dict[str, Any]: + return { + "sections": [ + "Competition Trade Plan", + "Candidate", + "Quote", + "Risk Verdict", + "Next Step", + ], + "neverShow": ["activityId", "accountId", "privateKey", "seedPhrase", "mnemonic"], + } + + +def _runbook(request: Dict[str, Any]) -> List[str]: + lines = [ + "Use onchainOS as the primary source for competition, token, quote, and execution data.", + "Treat Solana plus the competition primary chain as supported until backend exposes full multi-chain support.", + "Keep internal competition ids in tool context only; identify competitions to users by name.", + "Never execute before agent-risk-firewall check.", + "Never use --force.", + ] + if request["executionMode"] == "dry-run": + lines.append("Dry-run mode must not execute a transaction.") + return lines + + +def _uses_onchainos(steps: Iterable[Dict[str, Any]]) -> bool: + commands = " ".join(str(step.get("command") or "") for step in steps) + return "onchainos competition" in commands and "onchainos swap" in commands + + +def _issue(code: str, message: str) -> Dict[str, str]: + return {"code": code, "message": message} + + +def _stable_json(payload: Any) -> str: + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str) + + +def _sha256(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() diff --git a/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/render.py b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/render.py new file mode 100644 index 000000000..279ab2a5a --- /dev/null +++ b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/render.py @@ -0,0 +1,16 @@ +import json +from typing import Any, Dict, List + + +def dumps_json(payload: Any) -> str: + return json.dumps(payload, indent=2, sort_keys=False, ensure_ascii=False, default=str) + + +def error_payload(code: str, message: str, details: List[str] = None) -> Dict[str, Any]: + return { + "error": { + "code": code, + "message": message, + "details": details or [], + } + } diff --git a/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/risk_bridge.py b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/risk_bridge.py new file mode 100644 index 000000000..f65c658cf --- /dev/null +++ b/skills/safe-competition-trading-agent/src/safe_competition_trading_agent/risk_bridge.py @@ -0,0 +1,69 @@ +from typing import Any, Dict, List + +from .models import decimal_to_str + + +def build_firewall_input(request: Dict[str, Any], candidate: Dict[str, Any]) -> Dict[str, Any]: + wallet = request.get("wallet") or {} + wallet_address = wallet.get("address") or wallet.get("walletAddress") or "" + return { + "chain": request["chain"], + "operation": "swap", + "walletAddress": wallet_address, + "tokenIn": request.get("tokenIn") or {"symbol": "USDC"}, + "tokenOut": _token_for_firewall(candidate), + "amountIn": decimal_to_str(request.get("budgetUsd")), + "amountInUsd": decimal_to_str(request.get("budgetUsd")), + "quote": request.get("quote") or {}, + "tx": request.get("unsignedTx") or {}, + "competition": request.get("competition") or {}, + "policyProfile": "competition", + } + + +def risk_gate(request: Dict[str, Any]) -> Dict[str, Any]: + verdict = request.get("riskVerdict") or {} + value = str(verdict.get("verdict") or "unknown").lower() + reasons = verdict.get("reasons") if isinstance(verdict.get("reasons"), list) else [] + confirmed = bool(request.get("confirmed")) + + if value == "block": + return { + "okToExecute": False, + "state": "blocked", + "reasons": _reason_codes(reasons) or ["Firewall returned block."], + } + if value == "warn": + return { + "okToExecute": confirmed, + "state": "confirmed_warn" if confirmed else "needs_explicit_confirmation", + "reasons": _reason_codes(reasons) or ["Firewall returned warn."], + } + if value == "allow": + return { + "okToExecute": confirmed, + "state": "confirmed_allow" if confirmed else "ready_for_confirmation", + "reasons": [], + } + return { + "okToExecute": False, + "state": "risk_verdict_missing", + "reasons": ["Run agent-risk-firewall check before execution."], + } + + +def _token_for_firewall(candidate: Dict[str, Any]) -> Dict[str, Any]: + token = dict(candidate) + token.pop("selectionReasons", None) + token.pop("selectionScore", None) + return token + + +def _reason_codes(reasons: List[Any]) -> List[str]: + output = [] + for reason in reasons: + if isinstance(reason, dict): + output.append(str(reason.get("code") or reason.get("message") or reason)) + else: + output.append(str(reason)) + return output diff --git a/skills/safe-competition-trading-agent/tests/conftest.py b/skills/safe-competition-trading-agent/tests/conftest.py new file mode 100644 index 000000000..5f64b5314 --- /dev/null +++ b/skills/safe-competition-trading-agent/tests/conftest.py @@ -0,0 +1,5 @@ +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) diff --git a/skills/safe-competition-trading-agent/tests/test_candidate_selector.py b/skills/safe-competition-trading-agent/tests/test_candidate_selector.py new file mode 100644 index 000000000..69a0afdac --- /dev/null +++ b/skills/safe-competition-trading-agent/tests/test_candidate_selector.py @@ -0,0 +1,23 @@ +from safe_competition_trading_agent.candidate_selector import is_real_competition_token, select_candidate + + +def test_select_candidate_prefers_liquid_low_risk_real_token(): + request = { + "tokenOut": None, + "candidates": [ + {"symbol": "USDC", "liquidityUsd": "1000000", "riskLevel": "LOW"}, + {"symbol": "MEME", "liquidityUsd": "150000", "riskLevel": "LOW", "volume24hUsd": "75000"}, + {"symbol": "BAD", "riskLevel": "HIGH", "tags": ["honeypot"]}, + ], + } + + selected = select_candidate(request) + + assert selected["symbol"] == "MEME" + assert selected["selectionScore"] > 50 + + +def test_real_competition_token_excludes_stable_native(): + assert is_real_competition_token({"symbol": "MEME"}) is True + assert is_real_competition_token({"symbol": "USDC"}) is False + assert is_real_competition_token({"symbol": "SOL"}) is False diff --git a/skills/safe-competition-trading-agent/tests/test_planner.py b/skills/safe-competition-trading-agent/tests/test_planner.py new file mode 100644 index 000000000..62d4be371 --- /dev/null +++ b/skills/safe-competition-trading-agent/tests/test_planner.py @@ -0,0 +1,86 @@ +from safe_competition_trading_agent.planner import build_plan, dry_run, execute, template, validate_plan + + +def request(execution_mode="dry-run"): + payload = template("competition-safe-swap") + payload["executionMode"] = execution_mode + payload["tokenOut"] = {"symbol": "MEME", "liquidityUsd": "150000", "riskLevel": "LOW"} + return payload + + +def test_build_plan_uses_onchainos_competition_and_swap(): + plan = build_plan(request()) + commands = " ".join(str(step.get("command") or "") for step in plan["steps"]) + step_ids = [step["id"] for step in plan["steps"]] + + assert plan["validation"]["ok"] is True + assert "onchainos competition list" in commands + assert "onchainos swap quote" in commands + assert "agent-risk-firewall check" in commands + assert "execute_after_confirmation" not in step_ids + + +def test_confirm_before_execute_places_execution_after_firewall_and_confirmation(): + plan = build_plan(request("confirm-before-execute")) + step_ids = [step["id"] for step in plan["steps"]] + + assert plan["validation"]["ok"] is True + assert step_ids.index("risk_firewall_check") < step_ids.index("user_confirmation_gate") + assert step_ids.index("user_confirmation_gate") < step_ids.index("execute_after_confirmation") + + +def test_dry_run_reports_missing_competition_context_first(): + result = dry_run(request()) + + assert result["status"] == "needs_competition_context" + assert result["firewallInput"]["policyProfile"] == "competition" + + +def test_dry_run_ready_with_quote_competition_and_allow_verdict(): + payload = request() + payload["competition"] = {"active": True, "joined": True, "chainName": "X Layer", "supportedChains": ["xlayer", "solana"]} + payload["quote"] = {"slippagePct": 1, "priceImpactPct": 1} + payload["riskVerdict"] = {"verdict": "allow", "reasons": []} + + result = dry_run(payload) + + assert result["status"] == "ready" + assert result["riskGate"]["state"] == "ready_for_confirmation" + + +def test_execute_requires_confirmation_and_firewall_verdict(): + payload = request("confirm-before-execute") + payload["competition"] = {"active": True, "joined": True} + payload["quote"] = {"slippagePct": 1} + payload["riskVerdict"] = {"verdict": "allow"} + + blocked = execute(payload) + assert blocked["status"] == "blocked" + + payload["confirmed"] = True + ready = execute(payload) + assert ready["status"] == "ready-to-execute" + assert "onchainos swap execute" in ready["executionCommand"] + + +def test_execute_blocks_firewall_block_even_when_confirmed(): + payload = request("confirm-before-execute") + payload["confirmed"] = True + payload["riskVerdict"] = {"verdict": "block", "reasons": [{"code": "TOKEN_CRITICAL"}]} + + result = execute(payload) + + assert result["status"] == "blocked" + assert result["riskGate"]["state"] == "blocked" + + +def test_validate_rejects_execute_before_firewall(): + plan = build_plan(request("confirm-before-execute")) + steps = plan["steps"] + execute_step = [step for step in steps if step["id"] == "execute_after_confirmation"][0] + plan["steps"] = [execute_step] + [step for step in steps if step["id"] != "execute_after_confirmation"] + + result = validate_plan(plan) + + assert result["ok"] is False + assert any(error["code"] == "EXECUTE_BEFORE_FIREWALL" for error in result["errors"]) diff --git a/skills/safe-competition-trading-agent/tests/test_safe_competition_cli.py b/skills/safe-competition-trading-agent/tests/test_safe_competition_cli.py new file mode 100644 index 000000000..65d04544f --- /dev/null +++ b/skills/safe-competition-trading-agent/tests/test_safe_competition_cli.py @@ -0,0 +1,74 @@ +import json +import os +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def run_cli(args, input_text=None): + env = os.environ.copy() + env["PYTHONPATH"] = str(ROOT / "src") + env["PYTHONDONTWRITEBYTECODE"] = "1" + return subprocess.run( + [sys.executable, "-m", "safe_competition_trading_agent.cli"] + args, + input=input_text, + capture_output=True, + text=True, + cwd=str(ROOT), + env=env, + check=False, + ) + + +def request(): + return { + "intent": "Dry-run a safe competition trade.", + "chain": "xlayer", + "budgetUsd": "10", + "executionMode": "dry-run", + } + + +def test_plan_reads_stdin_and_outputs_json(): + completed = run_cli(["plan", "--input", "-", "--format", "json"], json.dumps(request())) + + assert completed.returncode == 0 + payload = json.loads(completed.stdout) + assert payload["strategyId"].startswith("scta_") + assert payload["validation"]["ok"] is True + + +def test_validate_returns_nonzero_for_bad_plan(): + bad_plan = {"executionMode": "dry-run", "steps": []} + completed = run_cli(["validate", "--input", "-", "--format", "json"], json.dumps(bad_plan)) + + assert completed.returncode == 1 + payload = json.loads(completed.stdout) + assert payload["ok"] is False + + +def test_template_command_outputs_request(): + completed = run_cli(["template", "--name", "rank-optimizer"]) + + assert completed.returncode == 0 + payload = json.loads(completed.stdout) + assert payload["goal"] == "rank-optimizer" + + +def test_malformed_json_returns_error_json(): + completed = run_cli(["plan", "--input", "-", "--format", "json"], "{bad json") + + assert completed.returncode != 0 + payload = json.loads(completed.stdout) + assert payload["error"]["code"] == "INVALID_JSON" + + +def test_self_test_passes(): + completed = run_cli(["self-test"]) + + assert completed.returncode == 0 + payload = json.loads(completed.stdout) + assert payload["status"] == "pass" From b57f3884351b46ba90127982256ff9baad0c106a Mon Sep 17 00:00:00 2001 From: Lumina Developer Date: Sat, 16 May 2026 15:58:30 +0700 Subject: [PATCH 2/2] docs: keep safe competition agent english ascii --- skills/safe-competition-trading-agent/SKILL.md | 11 ++++------- skills/safe-competition-trading-agent/plugin.yaml | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/skills/safe-competition-trading-agent/SKILL.md b/skills/safe-competition-trading-agent/SKILL.md index fa310363e..3c9efb7b7 100644 --- a/skills/safe-competition-trading-agent/SKILL.md +++ b/skills/safe-competition-trading-agent/SKILL.md @@ -42,13 +42,10 @@ Use this skill for requests like: - "dry-run competition trade" - "run a safe competition swap" - "join competition and trade" -- "giao dich cho cuoc thi" -- "trade cho competition" -- "tim token an toan de thi" -- "toi uu thu hang cuoc thi" -- "tang volume cuoc thi" -- "giao dich 10 do cho competition" -- "kiem tra trade nay co tinh diem khong" +- "check whether this competition trade counts" +- "find an eligible token trade" +- "prepare a guarded competition swap" +- "dry-run my Agentic Trading contest trade" Do not use this skill for generic wallet balance, normal token portfolio checks, exporting wallets, or unrelated DeFi positions. Use the corresponding wallet, portfolio, DeFi, or DEX skills for those. diff --git a/skills/safe-competition-trading-agent/plugin.yaml b/skills/safe-competition-trading-agent/plugin.yaml index e9f7b06a1..c44de81b7 100644 --- a/skills/safe-competition-trading-agent/plugin.yaml +++ b/skills/safe-competition-trading-agent/plugin.yaml @@ -23,7 +23,7 @@ components: build: lang: python source_repo: maixuancanh/agent-risk-firewall - source_commit: "dac401ca47dd1214965463b7b40551dada80ebaa" + source_commit: "21956e4cd1eb5f2a6c7b4b1b70b896f92deee93c" binary_name: safe-competition-trading-agent main: "src/safe_competition_trading_agent/cli.py"