diff --git a/.env.example b/.env.example index 8639d9f..8abd10d 100644 --- a/.env.example +++ b/.env.example @@ -218,6 +218,11 @@ PORT=8182 # VERCEL=1 # set automatically on Vercel # FORCE_SSL=false # true requires cert files +# CORS allowed origins — comma-separated list of allowed frontend origins. +# Defaults to the canonical SqueezeOS dashboard origins. +# Set to * for local development only — never in production. +# CORS_ORIGINS=https://scriptmasterlabs.com,https://www.scriptmasterlabs.com,https://signal-auction-loom.vercel.app + # ============================================================ # SMTP OUTREACH ENGINE — Automated Sales Outreach # ============================================================ diff --git a/.github/workflows/agent.yml b/.github/workflows/agent.yml index 9ba4604..9709216 100644 --- a/.github/workflows/agent.yml +++ b/.github/workflows/agent.yml @@ -18,9 +18,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.11' diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 42d8a6f..4624434 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -14,21 +14,21 @@ jobs: working-directory: mobile steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 cache: npm cache-dependency-path: mobile/package-lock.json - - uses: actions/setup-java@v5 + - uses: actions/setup-java@7a40b8b20c7b26c7e8a6c6fc36f62f01b9a74c45 # v4 with: distribution: temurin java-version: 17 - name: Setup Android SDK - uses: android-actions/setup-android@v4 + uses: android-actions/setup-android@d155f4e6b6ac79ae9f5b7873f72a649e5bb02db0 # v4 - name: Install dependencies run: npm ci @@ -85,14 +85,14 @@ jobs: -Pandroid.injected.signing.key.password="$KS_PASS" - name: Upload signed AAB - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: neural-os-${{ github.run_number }}-release.aab path: mobile/android/app/build/outputs/bundle/release/app-release.aab retention-days: 90 - name: Upload signed APK - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: neural-os-${{ github.run_number }}-release.apk path: mobile/android/app/build/outputs/apk/release/app-release.apk diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0fec8e8..887c651 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Smoke Test — Import Check +name: CI — Smoke Test, Lint, Security on: push: @@ -12,13 +12,16 @@ on: - '**.py' - 'requirements.txt' +permissions: + contents: read + jobs: imports: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.11' cache: pip @@ -89,3 +92,15 @@ jobs: if failures: sys.exit(1) " + + - name: Install security tools + run: pip install flake8 bandit safety + + - name: Lint (warnings only) + run: flake8 core/ --max-line-length=120 --ignore=E501,W503 || true + + - name: Security scan — bandit + run: bandit -r core/ -ll -q || true + + - name: Dependency vulnerability check + run: safety check -r requirements.txt || true diff --git a/.github/workflows/keepalive.yml b/.github/workflows/keepalive.yml index 4c2fcfc..10d5cf0 100644 --- a/.github/workflows/keepalive.yml +++ b/.github/workflows/keepalive.yml @@ -12,7 +12,7 @@ jobs: timeout-minutes: 5 steps: - # ── Ping all Render/Vercel services in parallel ─────────────── + # ── Ping all Render/Vercel services in parallel ─────────────────────── # Sequential steps let one slow cold-start block the others, so # downstream services miss their ping and fall asleep. Run all # pings concurrently with a 55 s timeout (just under the 5-min @@ -36,12 +36,12 @@ jobs: ping_service "SqueezeOS" "https://squeezeos-api.onrender.com/api/status" "200" & ping_service "SML Rails" "https://sml-rails.onrender.com/health" "200 404" & ping_service "TipMaster" "https://tipmaster.onrender.com/health" "200 404" & - ping_service "Ghost Layer Sovereign" "https://scriptmasterlabs.com" "200 301 302" & + ping_service "Ghost Layer Sovereign" "https://scriptmasterlabs.com" "200 301 302 403" & wait echo "All pings complete" - # ── Alert Discord only on genuine hard failures ─────────────── + # ── Alert Discord only on genuine hard failures ───────────────── - name: Alert Discord on failure if: failure() && env.DISCORD_WEBHOOK_ALL != '' env: diff --git a/.github/workflows/payment-smoke.yml b/.github/workflows/payment-smoke.yml index 57988a0..2a879bb 100644 --- a/.github/workflows/payment-smoke.yml +++ b/.github/workflows/payment-smoke.yml @@ -12,7 +12,7 @@ jobs: timeout-minutes: 10 steps: - # ── Step 1: Wake all Render services in parallel ───────────── + # ── Step 1: Wake all Render services in parallel ─────────────────── # Render free tier sleeps after 15 min; cold start takes 30–50 s. # Fire warm-up pings first so the real assertions don't time out. - name: Warm up Render services @@ -31,14 +31,14 @@ jobs: --max-time 60 https://four02proof.onrender.com/v1/stats) echo "body=$body" echo "$body" | python3 -c " - import sys, json - d = json.load(sys.stdin) - assert 'total_invoices' in d or 'invoices' in d or len(d) > 0, \ - f'Unexpected stats payload: {d}' - print(' OK 402Proof stats reachable and parseable') - " +import sys, json +d = json.load(sys.stdin) +assert 'total_invoices' in d or 'invoices' in d or len(d) > 0, \ + f'Unexpected stats payload: {d}' +print(' OK 402Proof stats reachable and parseable') +" - # ── Step 3: Request an invoice for /api/council ─────────────── + # ── Step 3: Request an invoice for /api/council ───────────────── - name: Request invoice id: invoice run: | @@ -50,12 +50,12 @@ jobs: "agent_wallet":"rSmokeTester000000000000000000001"}') echo "invoice_body=$body" echo "$body" | python3 -c " - import sys, json - d = json.load(sys.stdin) - assert 'invoice_id' in d or 'id' in d or 'pay_to' in d, \ - f'Invoice missing expected fields: {d}' - print(' OK Invoice generated — pay_to and amount present') - " +import sys, json +d = json.load(sys.stdin) +assert 'invoice_id' in d or 'id' in d or 'pay_to' in d, \ + f'Invoice missing expected fields: {d}' +print(' OK Invoice generated — pay_to and amount present') +" # ── Step 4: Ghost Layer health deep-check ──────────────────── - name: Ghost Layer health @@ -65,13 +65,13 @@ jobs: --max-time 60 https://ghost-layer.onrender.com/health) echo "ghost_body=$body" echo "$body" | python3 -c " - import sys, json - d = json.load(sys.stdin) - status = d.get('status', d.get('ok', d.get('alive', ''))) - assert status in ('ok', 'healthy', True, 'up', 'live', 'online') or len(d) > 0, \ - f'Ghost Layer health unexpected: {d}' - print(' OK Ghost Layer healthy') - " || echo " WARN Ghost Layer returned non-JSON — checking status code only" +import sys, json +d = json.load(sys.stdin) +status = d.get('status', d.get('ok', d.get('alive', ''))) +assert status in ('ok', 'healthy', True, 'up', 'live', 'online') or len(d) > 0, \ + f'Ghost Layer health unexpected: {d}' +print(' OK Ghost Layer healthy') +" || echo " WARN Ghost Layer returned non-JSON — checking status code only" # ── Step 5: SqueezeOS free demo (no payment needed) ────────── - name: SqueezeOS demo council @@ -81,14 +81,14 @@ jobs: --max-time 60 \ https://squeezeos-api.onrender.com/api/demo/council) echo "$body" | python3 -c " - import sys, json - d = json.load(sys.stdin) - assert d.get('status') == 'online' or 'bias' in d or 'verdict' in d or 'regime' in d, \ - f'Unexpected demo response: {d}' - print(' OK SqueezeOS demo council live') - " +import sys, json +d = json.load(sys.stdin) +assert d.get('status') == 'online' or 'bias' in d or 'verdict' in d or 'regime' in d, \ + f'Unexpected demo response: {d}' +print(' OK SqueezeOS demo council live') +" - # ── Alert if any step fails ─────────────────────────────────── + # ── Alert if any step fails ─────────────────────────────── - name: Alert Discord on smoke failure if: failure() && env.DISCORD_WEBHOOK_ALL != '' env: diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index e7443a3..ac4dae1 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -17,9 +17,9 @@ jobs: working-directory: 402proof/middleware/js steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '24' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 2df4b9c..6c3ae46 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -13,9 +13,9 @@ jobs: working-directory: 402proof/middleware/python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.11' diff --git a/.github/workflows/publish-squeezeos-to-mcp-registry.yml b/.github/workflows/publish-squeezeos-to-mcp-registry.yml index 7e0d78b..10f3c5f 100644 --- a/.github/workflows/publish-squeezeos-to-mcp-registry.yml +++ b/.github/workflows/publish-squeezeos-to-mcp-registry.yml @@ -22,7 +22,7 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install mcp-publisher run: | diff --git a/.github/workflows/service-monitor.yml b/.github/workflows/service-monitor.yml new file mode 100644 index 0000000..92d4e72 --- /dev/null +++ b/.github/workflows/service-monitor.yml @@ -0,0 +1,153 @@ +name: Service Health Monitor + +on: + schedule: + - cron: '*/10 * * * *' # every 10 min — proactive alerting before user notices + workflow_dispatch: + +jobs: + monitor: + name: Check all SML services + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Check services and alert on failure + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_ALL }} + run: | + FAILURES=() + WARNINGS=() + NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + check_service() { + local name="$1" + local url="$2" + local expected="${3:-200}" + + status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 --retry 1 "$url" 2>/dev/null) + + if [ -z "$status" ] || [ "$status" = "000" ]; then + FAILURES+=("$name|TIMEOUT|$url") + echo "FAIL $name → TIMEOUT (url: $url)" + elif echo "$expected" | grep -qw "$status"; then + echo " OK $name → $status" + elif [ "$status" = "403" ]; then + # 403 on Vercel = Deployment Protection gate is enabled — needs dashboard fix + WARNINGS+=("$name|403 Forbidden (Vercel auth gate — disable Deployment Protection in Vercel dashboard)|$url") + echo "WARN $name → 403 (Vercel Deployment Protection active)" + elif [ "$status" -ge 500 ] 2>/dev/null; then + FAILURES+=("$name|$status Server Error|$url") + echo "FAIL $name → $status (url: $url)" + else + FAILURES+=("$name|$status Unexpected|$url") + echo "FAIL $name → $status (url: $url)" + fi + } + + # ── All SML services ──────────────────────────────────────────── + check_service "Ghost Layer Sovereign (scriptmasterlabs.com)" \ + "https://scriptmasterlabs.com" "200 301 302" + + check_service "SqueezeOS API" \ + "https://squeezeos-api.onrender.com/api/status" "200" + + check_service "Ghost Layer" \ + "https://ghost-layer.onrender.com/health" "200" + + check_service "402Proof" \ + "https://four02proof.onrender.com/v1/stats" "200" + + check_service "SML Rails (RLUSD)" \ + "https://sml-rails.onrender.com/health" "200 404" + + check_service "SML Copy-Trader" \ + "https://sml-copytrader.onrender.com/health" "200 404" + + check_service "SML Launchpad" \ + "https://sml-launchpad.onrender.com/health" "200 404" + + check_service "TipMaster" \ + "https://tipmaster.onrender.com/health" "200 404" + + # ── Build Discord alert payload ──────────────────────────────── + if [ "${#FAILURES[@]}" -gt 0 ] || [ "${#WARNINGS[@]}" -gt 0 ]; then + if [ -z "$DISCORD_WEBHOOK" ]; then + echo "No DISCORD_WEBHOOK_ALL secret set — skipping alert" + else + FIELDS="[]" + + for entry in "${FAILURES[@]}"; do + IFS='|' read -r svc_name svc_status svc_url <<< "$entry" + FIELDS=$(echo "$FIELDS" | python3 -c " +import json, sys +fields = json.load(sys.stdin) +fields.append({ + 'name': '🔴 ' + '''$svc_name''', + 'value': '**Status:** ' + '''$svc_status''' + '\n**URL:** ' + '''$svc_url''', + 'inline': False +}) +print(json.dumps(fields)) +") + done + + for entry in "${WARNINGS[@]}"; do + IFS='|' read -r svc_name svc_status svc_url <<< "$entry" + FIELDS=$(echo "$FIELDS" | python3 -c " +import json, sys +fields = json.load(sys.stdin) +fields.append({ + 'name': '🟡 ' + '''$svc_name''', + 'value': '**Status:** ' + '''$svc_status''' + '\n**URL:** ' + '''$svc_url''', + 'inline': False +}) +print(json.dumps(fields)) +") + done + + COLOR=15158332 # red for hard failures + TITLE="🚨 SML Service Failure Detected" + if [ "${#FAILURES[@]}" -eq 0 ]; then + COLOR=16776960 # yellow for warnings only + TITLE="⚠️ SML Service Warning" + fi + + python3 - < Phase 1: no signer yet, will create one" fi - # ── PHASE 1 ───────────────────────────────────────────────────── + # ── PHASE 1 ──────────────────────────────────────────────── - name: "[P1] Create Neynar managed signer" if: steps.state.outputs.phase == '1' @@ -98,7 +98,7 @@ jobs: echo "" echo "Signer created and registered. Approval URL committed to repo." - # ── PHASE 2 ───────────────────────────────────────────────────── + # ── PHASE 2 ──────────────────────────────────────────────── - name: "[P2] Generate XRPL gateway wallet" if: steps.state.outputs.phase == '2' @@ -119,7 +119,6 @@ jobs: run: | UUID="${{ steps.state.outputs.signer_uuid }}" - # Check current status first GET0=$(curl -s "https://api.neynar.com/v2/farcaster/signer?signer_uuid=$UUID" -H "x-api-key: $NK") STATUS0=$(echo "$GET0" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null) PUBKEY=$(echo "$GET0" | python3 -c "import sys,json; print(json.load(sys.stdin).get('public_key',''))" 2>/dev/null) @@ -131,28 +130,23 @@ jobs: echo "fid=$FID" >> "$GITHUB_OUTPUT" echo "already_approved=true" >> "$GITHUB_OUTPUT" else - # Try GET signed_key_request — returns token used to build Warpcast approval URL echo "==> GET /signed_key_request for pubkey $PUBKEY" SKR=$(curl -s "https://api.neynar.com/v2/farcaster/signer/signed_key_request?key=${PUBKEY}" \ -H "x-api-key: $NK") echo "SKR response: $SKR" - # Try to extract token (used in deep link) TOKEN=$(echo "$SKR" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('token') or d.get('result',{}).get('token',''))" 2>/dev/null || true) echo "Token: $TOKEN" - # Check if approval URL already in signer GET response after above call GET1=$(curl -s "https://api.neynar.com/v2/farcaster/signer?signer_uuid=$UUID" -H "x-api-key: $NK") APPROVAL_URL=$(echo "$GET1" | python3 -c "import sys,json; print(json.load(sys.stdin).get('signer_approval_url',''))" 2>/dev/null || true) echo "Approval URL from GET: $APPROVAL_URL" - # Build from token if direct URL not available if [ -z "$APPROVAL_URL" ] && [ -n "$TOKEN" ]; then APPROVAL_URL="https://client.warpcast.com/deeplinks/signed-key-request?token=${TOKEN}" echo "Built approval URL from token: $APPROVAL_URL" fi - # Also try POST sponsored (might work on some plan tiers) if [ -z "$APPROVAL_URL" ]; then echo "==> Trying POST /signed_key with sponsored:true ..." REG=$(curl -s -X POST "https://api.neynar.com/v2/farcaster/signer/signed_key" \ @@ -163,7 +157,6 @@ jobs: echo "Approval URL from POST: $APPROVAL_URL" fi - # Commit all debug info + approval URL mkdir -p tipmaster-setup echo "$GET0" > tipmaster-setup/neynar_before.txt echo "$SKR" > tipmaster-setup/neynar_skr.txt diff --git a/.github/workflows/uptime-deploy.yml b/.github/workflows/uptime-deploy.yml index a9d61f6..6afa3a6 100644 --- a/.github/workflows/uptime-deploy.yml +++ b/.github/workflows/uptime-deploy.yml @@ -25,10 +25,10 @@ jobs: working-directory: uptime steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Node 22 - uses: actions/setup-node@v6 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "22" diff --git a/.github/workflows/xdeo-ci.yml b/.github/workflows/xdeo-ci.yml index 380e522..f6c09df 100644 --- a/.github/workflows/xdeo-ci.yml +++ b/.github/workflows/xdeo-ci.yml @@ -14,8 +14,8 @@ jobs: run: working-directory: xdeo steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: # Node >= 22.5 is required for the built-in node:sqlite module used by # the DB integration tests (test/helpers/d1.ts). diff --git a/.github/workflows/xdeo-deploy.yml b/.github/workflows/xdeo-deploy.yml index 849f14a..e7f683d 100644 --- a/.github/workflows/xdeo-deploy.yml +++ b/.github/workflows/xdeo-deploy.yml @@ -35,10 +35,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Node 22 - uses: actions/setup-node@v6 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "22" cache: npm diff --git a/.github/workflows/xdeo-publish-registry.yml b/.github/workflows/xdeo-publish-registry.yml index 429ccc9..c2dc204 100644 --- a/.github/workflows/xdeo-publish-registry.yml +++ b/.github/workflows/xdeo-publish-registry.yml @@ -25,7 +25,7 @@ jobs: working-directory: xdeo steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install mcp-publisher run: | diff --git a/.github/workflows/xdeo-web-ci.yml b/.github/workflows/xdeo-web-ci.yml index b3b8c4e..25153d8 100644 --- a/.github/workflows/xdeo-web-ci.yml +++ b/.github/workflows/xdeo-web-ci.yml @@ -16,8 +16,8 @@ jobs: env: NEXT_TELEMETRY_DISABLED: "1" steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "22" cache: npm diff --git a/.well-known/mcp.json b/.well-known/mcp.json index 77d1a1c..63bc65a 100644 --- a/.well-known/mcp.json +++ b/.well-known/mcp.json @@ -5,14 +5,14 @@ "url": "https://squeezeos-api.onrender.com/mcp", "transport": "streamable-http", "name": "SqueezeOS — Institutional Market Intelligence", - "description": "33 MCP tools for institutional-grade equity intelligence: squeeze scanner, options flow, IWM 0DTE, council verdicts, FTD data, institutional flows, convergence signals. Pay-per-call via x402 (RLUSD on XRPL or USDC on Base). Free tools included.", + "description": "35 MCP tools for institutional-grade equity intelligence: squeeze scanner, options flow, IWM 0DTE, council verdicts, IAM resolver, autopilot controls, convergence signals. Pay-per-call via x402 (RLUSD on XRPL or USDC on Base). Free tools included.", "version": "5.0.0" } }, "mcp_version": "1.0", "name": "sml-oracle", "display_name": "SqueezeOS — Institutional Market Intelligence (Script Master Labs)", - "description": "DISAMBIGUATION: This is SqueezeOS by Script Master Labs — institutional AI trading intelligence. NOT Logitech SqueezeOS (media player OS). 33 MCP tools: squeeze scanner, options flow, IWM 0DTE analysis, AI council verdicts, FTD data, convergence signals, institutional flows — all gated by x402 micropayment. No API keys required. Free tools included.", + "description": "DISAMBIGUATION: This is SqueezeOS by Script Master Labs — institutional AI trading intelligence. NOT Logitech SqueezeOS (media player OS). 35 MCP tools: squeeze scanner, options flow, IWM 0DTE analysis, AI council verdicts, IAM resolver, autopilot controls, convergence signals — all gated by x402 micropayment. No API keys required. Free tools included.", "homepage": "https://www.scriptmasterlabs.com", "mcp_endpoint": "https://squeezeos-api.onrender.com/mcp", "transport": "streamable-http", @@ -60,28 +60,22 @@ "convergence_check", "autopilot_status", "autopilot_trades", + "autopilot_start", + "autopilot_stop", + "circuit_breaker_reset", "beastmode_scan", "proprietary_ema_signal", "oracle_feeds", - "notary_info", - "notary_quote", - "triple_lock_demo", - "ftd_info" + "iam_truth" ], "paid_tools": [ - { "name": "council_verdict", "price_rlusd": 0.10 }, - { "name": "market_scan", "price_rlusd": 0.05 }, - { "name": "options_intelligence","price_rlusd": 0.05 }, - { "name": "iwm_odte", "price_rlusd": 0.03 }, - { "name": "triple_lock_verdict","price_rlusd": 0.25 }, - { "name": "marketplace_read_signal", "price_rlusd": 0.02 }, - { "name": "oracle_query", "price_rlusd": 0.02 }, - { "name": "ftd_threshold_list","price_rlusd": 0.02 }, - { "name": "ftd_series", "price_rlusd": 0.02 }, - { "name": "ftd_ratio", "price_rlusd": 0.03 }, - { "name": "ftd_etf_basket", "price_rlusd": 0.05 }, - { "name": "ftd_cycle", "price_rlusd": 0.05 }, - { "name": "notary_notarize", "price_rlusd": "0.001-0.050" } + { "name": "council_verdict", "price_rlusd": 0.10 }, + { "name": "market_scan", "price_rlusd": 0.05 }, + { "name": "options_intelligence", "price_rlusd": 0.05 }, + { "name": "iwm_odte", "price_rlusd": 0.03 }, + { "name": "marketplace_read_signal", "price_rlusd": 0.02 }, + { "name": "oracle_query", "price_rlusd": 0.02 }, + { "name": "iam_resolve", "price_rlusd": 0.05 } ], "agent_quick_start": [ "1. Call any free tool (e.g. demo_council, signal_preview) — no payment needed", diff --git a/Dockerfile b/Dockerfile index e7dbddd..3d120bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,5 +15,11 @@ COPY requirements.txt . RUN pip install --prefer-binary --no-cache-dir -r requirements.txt COPY . . + +# Create a non-root user and own the working directory +RUN adduser --system --no-create-home --group appuser \ + && chown -R appuser:appuser /app +USER appuser + EXPOSE 8182 -CMD ["gunicorn", "--bind", "0.0.0.0:8182", "--workers", "1", "--threads", "2", "--timeout", "120", "--max-requests", "500", "--max-requests-jitter", "50, core.app:create_app()"] +CMD ["gunicorn", "--bind", "0.0.0.0:8182", "--workers", "1", "--threads", "2", "--timeout", "120", "--max-requests", "500", "--max-requests-jitter", "50", "core.app:create_app()"] diff --git a/core/api/keys_bp.py b/core/api/keys_bp.py index 7498bf4..75e36f1 100644 --- a/core/api/keys_bp.py +++ b/core/api/keys_bp.py @@ -338,8 +338,8 @@ def checkout(): ) return redirect(session.url, code=303) except Exception as e: - logger.error(f"Stripe error: {e}") - return str(e), 500 + logger.error("Stripe checkout session error", exc_info=True) + return jsonify({"error": "internal error creating checkout session"}), 500 @keys_bp.route('/api/keys/success', methods=['GET']) @@ -355,7 +355,8 @@ def success(): return "No API key found for this session.", 404 return render_template_string(SUCCESS_HTML, api_key=api_key) except Exception as e: - return str(e), 500 + logger.error("Stripe session retrieval error", exc_info=True) + return jsonify({"error": "internal error retrieving session"}), 500 @keys_bp.route('/api/keys/webhook', methods=['POST']) diff --git a/core/api/premium_bp.py b/core/api/premium_bp.py index 41c2751..4202238 100644 --- a/core/api/premium_bp.py +++ b/core/api/premium_bp.py @@ -11,6 +11,7 @@ import sys import os +import re import time import logging import threading @@ -19,6 +20,18 @@ from core.state import state, sse_queues import core.signal_history as signal_history +_SYMBOL_RE = re.compile(r'^[A-Z0-9.]{1,10}$') + + +def _validate_symbol(raw: str) -> tuple: + """Return (cleaned_symbol, error_response_or_None). + Sanitizes and validates a ticker symbol input. + """ + cleaned = raw.upper().strip()[:10] + if not _SYMBOL_RE.match(cleaned): + return None, jsonify({"error": "invalid symbol", "message": "Symbol must be 1-10 uppercase alphanumeric characters"}), 400 + return cleaned, None + def _broadcast_sse(event: dict): """Push an event to all connected SSE clients.""" @@ -41,6 +54,16 @@ def _broadcast_sse(event: dict): logger = logging.getLogger("SqueezeOS-Premium") premium_bp = Blueprint('premium', __name__) +# Rate limiting — 30 requests/min per IP on premium endpoints +from core.rate_limiter import premium_limiter as _premium_rl +from flask import request as _req + +@premium_bp.before_request +def _rate_limit_premium(): + ip = _req.remote_addr or "unknown" + if not _premium_rl.allow(ip, _req.path): + return jsonify({"error": "rate_limit_exceeded", "message": "Too many requests. Retry after 60s."}), 429 + # ── /api/council ───────────────────────────────────────────────────────────── @@ -52,7 +75,10 @@ def council(): Returns regime, bias, risk score, and actionable thesis for a symbol or IWM. """ body = request.get_json(silent=True) or {} - symbol = (body.get('symbol') or request.args.get('symbol', 'IWM')).upper() + raw_symbol = body.get('symbol') or request.args.get('symbol', 'IWM') + symbol, err = _validate_symbol(raw_symbol) + if err: + return err dm = get_service('dm') if not dm: @@ -210,7 +236,10 @@ def options_flow(): Default symbol: IWM """ body = request.get_json(silent=True) or {} - symbol = (body.get('symbol') or request.args.get('symbol', 'IWM')).upper() + raw_symbol = body.get('symbol') or request.args.get('symbol', 'IWM') + symbol, err = _validate_symbol(raw_symbol) + if err: + return err dm = get_service('dm') if not dm: diff --git a/core/app.py b/core/app.py index bab0a7a..aa05b31 100644 --- a/core/app.py +++ b/core/app.py @@ -93,7 +93,16 @@ def create_app(): # Use parent directory as static folder to serve root files (index.html, .js, .css) root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) app = Flask(__name__, static_folder=root_dir, static_url_path='') - CORS(app) # Enable CORS for institutional dashboard + # CORS — restrict to known frontends in production. + # CORS_ORIGINS env var accepts a comma-separated list; defaults to the + # canonical dashboard origins. Set to "*" locally if needed for dev. + _cors_origins_env = os.environ.get( + "CORS_ORIGINS", + "https://scriptmasterlabs.com,https://www.scriptmasterlabs.com," + "https://signal-auction-loom.vercel.app,https://squeezeos-api.onrender.com", + ) + _cors_origins = [o.strip() for o in _cors_origins_env.split(",") if o.strip()] + CORS(app, origins=_cors_origins, supports_credentials=False) # Start Legacy Workers & Services (skipped in Vercel serverless mode) if not _IS_SERVERLESS: @@ -211,7 +220,10 @@ def run_agent_interceptor(response): def add_security_headers(response): response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['X-Content-Type-Options'] = 'nosniff' - response.headers['X-Frame-Options'] = 'SAMEORIGIN' + response.headers['X-Frame-Options'] = 'DENY' + response.headers['X-XSS-Protection'] = '1; mode=block' + response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' + response.headers['Content-Security-Policy'] = "default-src 'self'" response.headers['Link'] = '; rel="payment"' if 'text/html' in response.content_type: response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' diff --git a/core/rate_limiter.py b/core/rate_limiter.py new file mode 100644 index 0000000..68d4da6 --- /dev/null +++ b/core/rate_limiter.py @@ -0,0 +1,73 @@ +""" +Simple in-memory rate limiter for SqueezeOS endpoints. + +Uses a fixed-window counter per (IP, route_key) pair. +Thread-safe via a lock. Resets counters when the window expires. + +Usage (in a blueprint or app factory): + + from core.rate_limiter import RateLimiter + _rl = RateLimiter(limit=60, window=60) # 60 req/min + + @blueprint.before_request + def _check_rate(): + ip = request.remote_addr or "unknown" + if not _rl.allow(ip, request.path): + return jsonify({"error": "rate_limit_exceeded", "retry_after": _rl.window}), 429 +""" + +import time +import threading +import logging +from collections import defaultdict +from typing import Dict, Tuple + +logger = logging.getLogger(__name__) + + +class RateLimiter: + """Fixed-window in-memory rate limiter. + + Args: + limit: Maximum number of requests allowed per window per (ip, key). + window: Window duration in seconds. + """ + + def __init__(self, limit: int = 60, window: int = 60) -> None: + self.limit = limit + self.window = window + # _counters[(ip, key)] = (count, window_start_ts) + self._counters: Dict[Tuple[str, str], Tuple[int, float]] = defaultdict(lambda: (0, time.time())) + self._lock = threading.Lock() + + def allow(self, ip: str, key: str = "") -> bool: + """Return True if the request should be allowed, False if rate-limited.""" + bucket = (ip, key) + now = time.time() + with self._lock: + count, start = self._counters[bucket] + if now - start >= self.window: + # New window — reset + self._counters[bucket] = (1, now) + return True + if count >= self.limit: + return False + self._counters[bucket] = (count + 1, start) + return True + + def cleanup(self) -> None: + """Remove stale buckets older than 2 windows. Call periodically if needed.""" + cutoff = time.time() - self.window * 2 + with self._lock: + stale = [k for k, (_, start) in self._counters.items() if start < cutoff] + for k in stale: + del self._counters[k] + + +# ── Shared rate limiter instances ──────────────────────────────────────────── +# Premium endpoints: 30 req/min per IP (cost gate is the primary guard, this +# protects the invoice/payment verification machinery from DoS) +premium_limiter = RateLimiter(limit=30, window=60) + +# Free compute-heavy endpoints: 120 req/min per IP +free_limiter = RateLimiter(limit=120, window=60) diff --git a/llms.txt b/llms.txt index 7b8244e..37ac54d 100644 --- a/llms.txt +++ b/llms.txt @@ -1,19 +1,80 @@ # SqueezeOS MCP Server -> Institutional market intelligence MCP server. 33 tools. x402 payment gating. +> Institutional-grade AI trading intelligence platform. 35 MCP tools. Pay-per-call premium data via RLUSD on the XRP Ledger. No API keys, no subscriptions. ## Connect - MCP Endpoint: https://squeezeos-api.onrender.com/mcp -- Transport: streamable-http -- Auth: x402 (XRPL/RLUSD or Base/USDC) +- Transport: streamable-http (JSON-RPC 2.0) +- Health Check: GET https://squeezeos-api.onrender.com/api/status +- MCP Config: {"mcpServers":{"squeezeos":{"url":"https://squeezeos-api.onrender.com/mcp","transport":"streamable-http"}}} -## Free Tools -- demo_council - market regime overview -- signal_preview - signal teaser -- convergence_check - multi-timeframe check +## Payment Flow (Premium Tools) +1. Call `get_invoice` with the tool name you want — receive an RLUSD payment address + amount +2. Pay RLUSD on the XRP Ledger to the returned address +3. Call `verify_payment` with the transaction hash — receive a signed JWT token +4. Call the premium tool with `payment_token` argument +- Payment Firewall: https://four02proof.onrender.com +- Currency: RLUSD (XRP Ledger stablecoin) +- Token lifespan: 1 hour from issuance -## Paid Tools (x402) -- squeeze_scan - short squeeze detection $0.01 -- ftd_cycle - FTD cycle analysis $0.05 -- full_regime - institutional regime $0.25 +## Free Tools (no payment required) +- `demo_council` — IWM market regime overview (5-min cached) +- `signal_preview` — bias + regime teaser for any symbol (15-min cached) +- `signal_history` — recent signal ring buffer (last 200 per symbol) +- `get_invoice` — generate a payment invoice for any premium tool +- `verify_payment` — confirm RLUSD payment and receive JWT token +- `bureau_public_score` — agent credit bureau public score by wallet +- `marketplace_browse` — browse peer signal listings +- `marketplace_list_signal` — list your signal thesis for sale +- `hiring_browse_jobs` — browse agent job board +- `hiring_post_job` — post a job for agents +- `system_status` — system health, uptime, tool count, engine status +- `futures_create` — stake RLUSD on a verdict prediction +- `futures_take` — take the other side of a futures position +- `futures_browse` — browse open signal futures +- `futures_leaderboard` — top predictors by wallet +- `convergence_check` — multi-timeframe convergence analysis +- `autopilot_status` — Sovereign Autopilot (CEO Trader) current status +- `autopilot_trades` — recent autonomous trade history +- `beastmode_scan` — lightweight squeeze candidate scan +- `proprietary_ema_signal` — SML fractal EMA signal +- `settlement_create` — create a conditional escrow contract +- `settlement_browse` — browse open conditional contracts +- `settlement_trigger` — trigger a settlement condition check +- `oracle_feeds` — all active oracle directives batch +- `iam_truth` — Inevitable Action Model: read the current forced-action state (free preview) -## Built by ScriptMasterLabs (SDVOSB) - ScriptMasterLabs@gmail.com +## Paid Tools (RLUSD via x402) +- `council_verdict` — 0.10 RLUSD — Full multi-engine AI verdict for any symbol (OracleEngine + RDT + SML + MarketGraph + CEOTrader) +- `market_scan` — 0.05 RLUSD — Full $1–$50 squeeze scanner, all active tickers +- `options_intelligence` — 0.05 RLUSD — Institutional options flow scanner +- `iwm_odte` — 0.03 RLUSD — IWM zero-day-to-expiry contract scorer +- `marketplace_read_signal` — 0.02 RLUSD — Full signal thesis from peer marketplace +- `oracle_query` — 0.02 RLUSD — Deep oracle query with regime + confidence breakdown +- `iam_resolve` — 0.05 RLUSD — Inevitable Action Model: resolve the forced market action + entry point + +## Operator-Only Tools (X-Operator-Key header required) +- `autopilot_start` — Start the Sovereign Autopilot autonomous trader +- `autopilot_stop` — Stop the Sovereign Autopilot +- `circuit_breaker_reset` — Reset circuit breaker after drawdown halt + +## Signal Futures Market +Agents can stake RLUSD on what the next council verdict will be for a symbol. +Platform fee: 5%. Max 2000 futures globally, 30 per wallet. +Valid symbols: IWM SPY QQQ GME AMC MSTR NVDA TSLA PLTR HOOD + +## Conditional Settlement (Zero Custody) +SqueezeOS tracks intent and proof only. Escrow is executed agent-to-agent. +Conditions: bias_match, confidence_above, price_above, price_below, time_elapsed. +Platform fee: 1% on settlement. + +## Peer Marketplace +Free to list. 0.02 RLUSD to read full signal thesis. +Each sale grants +2 Agent Credit Bureau score to the seller. + +## Key Headers +- X-Payment-Token: — for paid tool calls +- X-Agent-Wallet: 0x... or rXRP... — for wallet-gated features and loyalty tracking + +## Built by ScriptMasterLabs (SDVOSB) +Contact: ScriptMasterLabs@gmail.com +Live: https://www.scriptmasterlabs.com diff --git a/proof402_integration.py b/proof402_integration.py index a3f5e9e..fed4547 100644 --- a/proof402_integration.py +++ b/proof402_integration.py @@ -266,9 +266,11 @@ def decorated(*args, **kwargs): request_wallet = request.headers.get('X-Agent-Wallet', '') if token_wallet and request_wallet and token_wallet != request_wallet: + # Mask wallet addresses in logs: show first-6...last-4 only + _mask = lambda w: f"{w[:6]}...{w[-4:]}" if len(w) > 10 else "***" _logging.warning( '[402Proof] wallet mismatch — token_wlt=%s request_wlt=%s path=%s', - token_wallet, request_wallet, path + _mask(token_wallet), _mask(request_wallet), path ) if _ENFORCE_WALLET_BINDING: return jsonify({ @@ -308,8 +310,12 @@ def decorated(*args, **kwargs): try: inv = _issue_invoice(endpoint_id) except Exception as e: - _logging.warning(f'[402Proof] invoice fetch failed: {e} — passing through') - return f(*args, **kwargs) + _logging.error(f'[402Proof] invoice fetch failed: {e} — payment gate CLOSED (503)') + return jsonify({ + 'error': 'ERR_PAYMENT_GATE_UNAVAILABLE', + 'message': 'Payment gateway temporarily unavailable. Cannot issue invoice. Retry later.', + 'retry_after': 30, + }), 503 _base = os.getenv('SQUEEZEOS_BASE_URL', 'https://squeezeos-api.onrender.com') free_preview = _free_preview_for(path) diff --git a/render.yaml b/render.yaml index b3564a0..85e2693 100644 --- a/render.yaml +++ b/render.yaml @@ -16,7 +16,7 @@ services: - key: SQUEEZEOS_OPERATOR_WALLET value: "rsdSmbUASmWDs4NPtE8ZHoMKmk1owR1yUh" - key: DISCORD_WEBHOOK_PAYMENTS - value: "https://discord.com/api/webhooks/1501375153584738306/JxIBorw9Eiw4mY1xfrlsTOrBqCi_BS-CRL6OgsDdFSG-PCFZH0cf7g70Q_m0SYRdK_nK" + sync: false - key: DISCORD_WEBHOOK_URL sync: false - key: DISCORD_WEBHOOK_ALL @@ -83,7 +83,6 @@ services: # --------------------------------------------------------------------------- # outbound-hunter — 24/7 Registry Broadcaster + Agent-to-Agent Hustler - # Continuously seeds AI registries and delivers signed samples to new bots. # --------------------------------------------------------------------------- - type: worker name: sml-outbound-hunter @@ -119,7 +118,6 @@ services: # --------------------------------------------------------------------------- # pne-gateway — Signal Loom sovereign intent auction gateway (Rust/Axum) - # L402 priority bidding over SqueezeOS API — formerly on Railway, now Render # --------------------------------------------------------------------------- - type: web name: pne-gateway