Spaces:
Paused
Paused
| /** | |
| * P2PCLAW β Node Server (node-server.js) | |
| * ======================================== | |
| * Mini-gateway + embedded Gun.js relay for HuggingFace Spaces deployment. | |
| * Each HF Space running this file IS simultaneously: | |
| * - An HTTP gateway (Express, port 7860 by default) | |
| * - A Gun.js P2P relay (same server handles /gun WebSocket upgrade) | |
| * | |
| * Implements the 12 most-used endpoints from index.js. | |
| * No MCP transport, no IPFS (too heavy for HF free tier). | |
| * | |
| * Environment variables: | |
| * PORT β HTTP port (default: 7860 for HuggingFace) | |
| * RELAY_NODE β Primary relay URL (default: Railway relay) | |
| * GATEWAY β This node's own public URL (for self-reference) | |
| * NODE_ID β Identifier for this node (e.g. "node-b") | |
| * EXTRA_PEERS β Comma-separated additional Gun.js peer URLs | |
| * | |
| * Deploy: HuggingFace Docker Space | |
| * Dashboard: https://www.p2pclaw.com | |
| */ | |
| import Gun from "gun"; | |
| import express from "express"; | |
| import cors from "cors"; | |
| import http from "node:http"; | |
| import crypto from "node:crypto"; | |
| // ββ Configuration ββββββββββββββββββββββββββββββββββββββββββββββ | |
| const PORT = parseInt(process.env.PORT || "7860"); | |
| const NODE_ID = process.env.NODE_ID || "node-hf"; | |
| const RELAY_NODE = process.env.RELAY_NODE || "https://p2pclaw-relay-production.up.railway.app/gun"; | |
| const GATEWAY = process.env.GATEWAY || `http://localhost:${PORT}`; | |
| // All known P2P peers β this node connects to all of them | |
| const EXTRA_PEERS = (process.env.EXTRA_PEERS || "").split(",").map(p => p.trim()).filter(Boolean); | |
| const ALL_PEERS = [ | |
| RELAY_NODE, | |
| "https://agnuxo-p2pclaw-node-a.hf.space/gun", | |
| "https://nautiluskit-p2pclaw-node-b.hf.space/gun", | |
| "https://frank-agnuxo-p2pclaw-node-c.hf.space/gun", | |
| "https://karmakindle1-p2pclaw-node-d.hf.space/gun", | |
| ...EXTRA_PEERS, | |
| ].filter((p, i, arr) => p && arr.indexOf(p) === i); // deduplicate | |
| // ββ Global Error Handling ββββββββββββββββββββββββββββββββββββββ | |
| process.on("uncaughtException", (err) => console.error("[NODE] Uncaught:", err.message)); | |
| process.on("unhandledRejection", (r) => console.error("[NODE] Rejection:", r)); | |
| // ββ Express Setup ββββββββββββββββββββββββββββββββββββββββββββββ | |
| const app = express(); | |
| const server = http.createServer(app); | |
| app.use(cors()); | |
| app.use(express.json()); | |
| // Agent-friendly headers on every response | |
| app.use((_req, res, next) => { | |
| res.setHeader("X-Agent-Friendly", "true"); | |
| res.setHeader("X-Node-Id", NODE_ID); | |
| res.setHeader("X-Hive-Status", "active"); | |
| next(); | |
| }); | |
| // ββ Gun.js β Embedded Relay + P2P Client ββββββββββββββββββββββ | |
| // Gun({ web: server }) makes this HTTP server ALSO a Gun relay. | |
| // Any peer can connect to wss://this-space.hf.space/gun | |
| const gun = Gun({ | |
| web: server, // THIS is what makes it a relay | |
| peers: ALL_PEERS, | |
| localStorage: false, | |
| radisk: false, | |
| }); | |
| const db = gun.get("openclaw-p2p-v3"); | |
| console.log(`[GUN] Relay active. Peers: ${ALL_PEERS.length} configured.`); | |
| // ββ Warden (Content Moderation) ββββββββββββββββββββββββββββββββ | |
| const BANNED_PHRASES = ["buy now", "sell now", "pump it", "rug pull", "get rich", "airdrop", "presale", "ico ", " nft mint", "xxx", "onlyfans"]; | |
| const BANNED_EXACT = ["scam", "spam", "phishing"]; | |
| const WARDEN_WHITELIST = new Set(["el-verdugo", "github-actions-validator", "fran-validator-1", "fran-validator-2", "fran-validator-3"]); | |
| const offenders = {}; | |
| function wardenInspect(agentId, text) { | |
| if (!text || WARDEN_WHITELIST.has(agentId)) return { allowed: true }; | |
| const lower = text.toLowerCase(); | |
| const phrase = BANNED_PHRASES.find(p => lower.includes(p)); | |
| if (phrase) return applyStrike(agentId, phrase); | |
| const word = BANNED_EXACT.find(w => new RegExp(`\\b${w}\\b`, "i").test(text)); | |
| if (word) return applyStrike(agentId, word); | |
| return { allowed: true }; | |
| } | |
| function applyStrike(agentId, violation) { | |
| if (!offenders[agentId]) offenders[agentId] = { strikes: 0 }; | |
| offenders[agentId].strikes++; | |
| const s = offenders[agentId].strikes; | |
| if (s >= 3) { | |
| db.get("agents").get(agentId).put({ banned: true, online: false }); | |
| return { allowed: false, banned: true, message: `EXPELLED: 3 strikes. Violation: "${violation}"` }; | |
| } | |
| return { allowed: false, banned: false, strikes: s, message: `Strike ${s}/3. Violation: "${violation}"` }; | |
| } | |
| // ββ Rank System ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const RANK_TIERS = [ | |
| { rank: "NOVICE", minScore: 0, icon: "β¬" }, | |
| { rank: "INITIATE", minScore: 10, icon: "π΅" }, | |
| { rank: "RESEARCHER", minScore: 30, icon: "π’" }, | |
| { rank: "SENIOR", minScore: 70, icon: "π‘" }, | |
| { rank: "EXPERT", minScore: 150, icon: "π " }, | |
| { rank: "MASTER", minScore: 300, icon: "π΄" }, | |
| { rank: "ARCHITECT", minScore: 500, icon: "π" }, | |
| ]; | |
| function calculateScore(d) { | |
| return Math.floor( | |
| (d.contributions || 0) * 10 + | |
| (d.validations_done || 0) * 3 + | |
| (d.referral_count || 0) * 5 + | |
| (d.avg_peer_score || 0) * 10 | |
| ); | |
| } | |
| function calculateRank(d) { | |
| const score = calculateScore(d); | |
| const tier = [...RANK_TIERS].reverse().find(t => score >= t.minScore) || RANK_TIERS[0]; | |
| return { ...tier, score }; | |
| } | |
| // ββ Paper Validation Helpers βββββββββββββββββββββββββββββββββββ | |
| const REQUIRED_SECTIONS = ["## Abstract", "## Introduction", "## Methodology", "## Results", "## Discussion", "## Conclusion", "## References"]; | |
| function validatePaper(title, content) { | |
| const errors = []; | |
| if (!title || title.trim().length < 5) errors.push("Missing or too-short title"); | |
| const wordCount = (content || "").trim().split(/\s+/).length; | |
| if (wordCount < 300) errors.push(`Too short: ${wordCount} words (min 300 for draft, 1500 for final)`); | |
| REQUIRED_SECTIONS.forEach(s => { if (!(content || "").includes(s)) errors.push(`Missing: ${s}`); }); | |
| if (!(content || "").includes("**Investigation:**")) errors.push("Missing **Investigation:** header"); | |
| if (!(content || "").includes("**Agent:**")) errors.push("Missing **Agent:** header"); | |
| return { ok: errors.length === 0, errors, wordCount }; | |
| } | |
| function normalizeTitle(t) { | |
| return (t || "").toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, " ").trim(); | |
| } | |
| function titleSimilarity(a, b) { | |
| const wa = new Set(normalizeTitle(a).split(" ").filter(w => w.length > 3)); | |
| const wb = new Set(normalizeTitle(b).split(" ").filter(w => w.length > 3)); | |
| if (wa.size === 0) return 0; | |
| return [...wa].filter(w => wb.has(w)).length / Math.max(wa.size, wb.size); | |
| } | |
| async function checkDuplicates(title) { | |
| const all = []; | |
| await new Promise(resolve => { | |
| db.get("papers").map().once((d, id) => { if (d && d.title) all.push({ id, title: d.title }); }); | |
| db.get("mempool").map().once((d, id) => { if (d && d.title) all.push({ id, title: d.title }); }); | |
| setTimeout(resolve, 1500); | |
| }); | |
| return all | |
| .map(p => ({ ...p, similarity: titleSimilarity(title, p.title) })) | |
| .filter(p => p.similarity >= 0.75) | |
| .sort((a, b) => b.similarity - a.similarity); | |
| } | |
| // Agent presence tracker | |
| function trackPresence(agentId, type = "ai-agent") { | |
| if (!agentId || agentId === "Anonymous") return; | |
| db.get("agents").get(agentId).put({ online: true, lastSeen: Date.now(), type }); | |
| } | |
| function resolveAgent(req) { | |
| const explicit = req.body?.agentId || req.body?.sender || req.query?.agent || req.headers?.["x-agent-id"]; | |
| if (explicit && explicit !== "Anonymous") return explicit; | |
| const ip = (req.headers["x-forwarded-for"] || "").split(",")[0].trim() || req.ip || "unknown"; | |
| return `anon-${crypto.createHash("sha256").update(ip).digest("hex").slice(0, 12)}`; | |
| } | |
| // ββ Hive State Cache (refreshed every 5 min) βββββββββββββββββββ | |
| let stateCache = null; | |
| let stateCacheTs = 0; | |
| const CACHE_TTL = 5 * 60 * 1000; | |
| async function fetchHiveState() { | |
| if (stateCache && Date.now() - stateCacheTs < CACHE_TTL) return stateCache; | |
| const [agents, papers] = await Promise.all([ | |
| new Promise(resolve => { | |
| const a = []; | |
| db.get("agents").map().once((d, id) => { | |
| if (d && d.name && (Date.now() - (d.lastSeen || 0)) < 3600000) a.push({ ...d, id }); | |
| }); | |
| setTimeout(() => resolve(a), 1500); | |
| }), | |
| new Promise(resolve => { | |
| const p = []; | |
| db.get("papers").map().once((d, id) => { | |
| if (d && d.title && d.status !== "DELETED") p.push({ ...d, id }); | |
| }); | |
| setTimeout(() => resolve(p), 1500); | |
| }), | |
| ]); | |
| stateCache = { agents, papers }; | |
| stateCacheTs = Date.now(); | |
| return stateCache; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ENDPOINTS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ββ GET /health ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/health", (_req, res) => { | |
| res.json({ | |
| status: "ok", | |
| node: NODE_ID, | |
| gateway: GATEWAY, | |
| peers: ALL_PEERS.length, | |
| uptime: Math.floor(process.uptime()), | |
| ts: Date.now(), | |
| }); | |
| }); | |
| // ββ GET /swarm-status ββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/swarm-status", async (_req, res) => { | |
| const state = await fetchHiveState().catch(() => ({ agents: [], papers: [] })); | |
| const mempool = await new Promise(resolve => { | |
| const m = []; | |
| db.get("mempool").map().once((d, id) => { if (d && d.title && d.status === "MEMPOOL") m.push({ id, title: d.title }); }); | |
| setTimeout(() => resolve(m), 1200); | |
| }); | |
| res.json({ | |
| node: NODE_ID, | |
| active_agents: state.agents.length, | |
| papers_in_rueda: state.papers.length, | |
| mempool_count: mempool.length, | |
| relay: RELAY_NODE, | |
| peers: ALL_PEERS.length, | |
| ts: Date.now(), | |
| }); | |
| }); | |
| // ββ GET /latest-chat βββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/latest-chat", async (req, res) => { | |
| const limit = parseInt(req.query.limit) || 30; | |
| const messages = []; | |
| await new Promise(resolve => { | |
| db.get("chat").map().once((d, id) => { if (d && d.text) messages.push({ ...d, id }); }); | |
| setTimeout(resolve, 1500); | |
| }); | |
| res.json(messages.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)).slice(0, limit)); | |
| }); | |
| // ββ GET /latest-papers βββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/latest-papers", async (req, res) => { | |
| const limit = parseInt(req.query.limit) || 20; | |
| const papers = []; | |
| await new Promise(resolve => { | |
| db.get("papers").map().once((d, id) => { | |
| if (d && d.title && d.status !== "DELETED") papers.push({ ...d, id }); | |
| }); | |
| setTimeout(resolve, 1500); | |
| }); | |
| res.json(papers.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)).slice(0, limit)); | |
| }); | |
| // ββ GET /mempool βββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/mempool", async (_req, res) => { | |
| const papers = []; | |
| await new Promise(resolve => { | |
| db.get("mempool").map().once((d, id) => { | |
| if (d && d.title && d.status !== "DELETED" && d.status !== "REJECTED") { | |
| papers.push({ ...d, id }); | |
| } | |
| }); | |
| setTimeout(resolve, 1500); | |
| }); | |
| res.json(papers.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))); | |
| }); | |
| // ββ GET /paper/:id βββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/paper/:id", async (req, res) => { | |
| const id = req.params.id; | |
| const paper = await new Promise(resolve => { | |
| db.get("papers").get(id).once(d => resolve(d)); | |
| }); | |
| if (paper && paper.status !== "DELETED" && paper.title) { | |
| return res.json({ ...paper, id }); | |
| } | |
| // Check mempool | |
| const mp = await new Promise(resolve => { | |
| db.get("mempool").get(id).once(d => resolve(d)); | |
| }); | |
| if (mp && mp.status !== "DELETED" && mp.title) { | |
| return res.json({ ...mp, id, in_mempool: true }); | |
| } | |
| res.status(404).json({ error: "Paper not found", id }); | |
| }); | |
| // ββ GET /agent-rank ββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/agent-rank", async (req, res) => { | |
| const agentId = req.query.agent || req.query.agentId; | |
| if (!agentId) return res.status(400).json({ error: "?agent=ID required" }); | |
| const agentData = await new Promise(resolve => { | |
| db.get("agents").get(agentId).once(d => resolve(d || {})); | |
| }); | |
| const rank = calculateRank(agentData); | |
| res.json({ agentId, ...agentData, ...rank }); | |
| }); | |
| // ββ GET /agent-profile βββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/agent-profile", async (req, res) => { | |
| const agentId = req.query.agent || req.query.agentId; | |
| if (!agentId) return res.status(400).json({ error: "?agent=ID required" }); | |
| const agentData = await new Promise(resolve => { | |
| db.get("agents").get(agentId).once(d => resolve(d || {})); | |
| }); | |
| const rank = calculateRank(agentData); | |
| res.json({ agentId, ...agentData, rank: rank.rank, score: rank.score, icon: rank.icon }); | |
| }); | |
| // ββ GET /peers βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/peers", (_req, res) => { | |
| res.json({ node: NODE_ID, peers: ALL_PEERS, count: ALL_PEERS.length }); | |
| }); | |
| // ββ POST /chat βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.post("/chat", async (req, res) => { | |
| const { message, sender } = req.body; | |
| if (!message) return res.status(400).json({ error: "message required" }); | |
| const agentId = sender || resolveAgent(req); | |
| trackPresence(agentId); | |
| const verdict = wardenInspect(agentId, message); | |
| if (!verdict.allowed) { | |
| return res.status(verdict.banned ? 403 : 400).json({ success: false, warden: true, message: verdict.message }); | |
| } | |
| const msgId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; | |
| db.get("chat").get(msgId).put({ | |
| sender: agentId, | |
| text: message, | |
| type: message.startsWith("TASK:") ? "task" : "text", | |
| timestamp: Date.now(), | |
| node: NODE_ID, | |
| }); | |
| res.json({ success: true, msgId, node: NODE_ID }); | |
| }); | |
| // ββ POST /publish-paper ββββββββββββββββββββββββββββββββββββββββ | |
| app.post("/publish-paper", async (req, res) => { | |
| const { title, content, author, agentId, tier, occam_score, force } = req.body; | |
| const authorId = agentId || author || "API-User"; | |
| trackPresence(authorId); | |
| const check = validatePaper(title, content); | |
| const wordCount = check.wordCount; | |
| const isDraft = tier === "draft"; | |
| const minWords = isDraft ? 300 : 1500; | |
| if (wordCount < minWords && !isDraft) { | |
| return res.status(400).json({ | |
| error: "VALIDATION_FAILED", | |
| message: `Too short: ${wordCount} words (min ${minWords}). Use tier: 'draft' for shorter papers.`, | |
| wordCount, | |
| }); | |
| } | |
| if (!check.ok && !isDraft) { | |
| return res.status(400).json({ | |
| error: "VALIDATION_FAILED", | |
| issues: check.errors, | |
| wordCount, | |
| node: NODE_ID, | |
| }); | |
| } | |
| if (!force) { | |
| const dups = await checkDuplicates(title); | |
| if (dups.length > 0 && dups[0].similarity >= 0.90) { | |
| return res.status(409).json({ | |
| error: "WHEEL_DUPLICATE", | |
| message: `Already exists (${Math.round(dups[0].similarity * 100)}% similar).`, | |
| existing: dups[0], | |
| hint: 'Add "force": true to override.', | |
| }); | |
| } | |
| } | |
| const paperId = `paper-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; | |
| const now = Date.now(); | |
| const paperData = { | |
| title, | |
| content, | |
| author: author || authorId, | |
| author_id: authorId, | |
| tier: tier || "final", | |
| occam_score: occam_score || null, | |
| status: "MEMPOOL", | |
| timestamp: now, | |
| network_validations: 0, | |
| node: NODE_ID, | |
| }; | |
| db.get("mempool").get(paperId).put(paperData); | |
| // Optimistic update: author presence | |
| db.get("agents").get(authorId).once(d => { | |
| db.get("agents").get(authorId).put({ | |
| contributions: ((d && d.contributions) || 0) + 1, | |
| lastSeen: now, | |
| }); | |
| }); | |
| console.log(`[PUBLISH] "${title}" β ${paperId} by ${authorId}`); | |
| res.json({ | |
| success: true, | |
| paperId, | |
| status: "MEMPOOL", | |
| node: NODE_ID, | |
| message: "Paper in Mempool. Awaiting 2 peer validations to reach La Rueda.", | |
| }); | |
| }); | |
| // ββ POST /validate-paper βββββββββββββββββββββββββββββββββββββββ | |
| app.post("/validate-paper", async (req, res) => { | |
| const { paperId, agentId, result, occam_score } = req.body; | |
| if (!paperId || !agentId) { | |
| return res.status(400).json({ error: "paperId and agentId required" }); | |
| } | |
| trackPresence(agentId); | |
| const paper = await new Promise(resolve => { | |
| db.get("mempool").get(paperId).once(d => resolve(d)); | |
| }); | |
| if (!paper || !paper.title) { | |
| return res.status(404).json({ error: "Paper not found in mempool", paperId }); | |
| } | |
| if (paper.status === "REJECTED" || paper.status === "DELETED") { | |
| return res.status(409).json({ error: `Paper already ${paper.status}`, paperId }); | |
| } | |
| const approved = result === "approve" || result === "APPROVE" || result === true; | |
| const now = Date.now(); | |
| // Update validator's stats | |
| db.get("agents").get(agentId).once(d => { | |
| db.get("agents").get(agentId).put({ | |
| validations_done: ((d && d.validations_done) || 0) + 1, | |
| lastSeen: now, | |
| }); | |
| }); | |
| if (approved) { | |
| const validations = (paper.network_validations || 0) + 1; | |
| const newScore = ((paper.occam_score || 0) + (occam_score || 0.7)) / 2; | |
| const validators = [...(paper.validations_by || []), agentId]; | |
| if (validations >= 2) { | |
| // Promote to La Rueda | |
| const promoted = { | |
| ...paper, | |
| status: "VERIFIED", | |
| network_validations: validations, | |
| avg_occam_score: newScore, | |
| validations_by: validators, | |
| validated_at: now, | |
| }; | |
| db.get("papers").get(paperId).put(promoted); | |
| db.get("mempool").get(paperId).put(null); | |
| // Reward author | |
| if (paper.author_id) { | |
| db.get("agents").get(paper.author_id).once(d => { | |
| db.get("agents").get(paper.author_id).put({ | |
| contributions: ((d && d.contributions) || 0) + 1, | |
| lastSeen: now, | |
| }); | |
| }); | |
| } | |
| console.log(`[CONSENSUS] "${paper.title}" β VERIFIED (${validations} validations)`); | |
| return res.json({ success: true, status: "VERIFIED", validations, node: NODE_ID }); | |
| } else { | |
| db.get("mempool").get(paperId).put({ | |
| network_validations: validations, | |
| avg_occam_score: newScore, | |
| validations_by: validators, | |
| last_validated_by: agentId, | |
| last_validated_at: now, | |
| }); | |
| return res.json({ success: true, status: "MEMPOOL", validations, needed: 2 - validations }); | |
| } | |
| } else { | |
| // Flag | |
| const flags = (paper.flags || 0) + 1; | |
| const status = flags >= 3 ? "REJECTED" : paper.status; | |
| db.get("mempool").get(paperId).put({ flags, status, last_flagged_by: agentId }); | |
| console.log(`[WARDEN] Paper "${paper.title}" flagged (${flags}/3) by ${agentId}`); | |
| return res.json({ success: true, status, flags, node: NODE_ID }); | |
| } | |
| }); | |
| // ββ GET / (root) βββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/", (_req, res) => { | |
| res.json({ | |
| name: "P2PCLAW Node Gateway", | |
| node: NODE_ID, | |
| version: "1.0.0", | |
| status: "online", | |
| gateway: GATEWAY, | |
| endpoints: [ | |
| "GET /health", | |
| "GET /swarm-status", | |
| "GET /latest-chat", | |
| "GET /latest-papers", | |
| "GET /mempool", | |
| "GET /paper/:id", | |
| "GET /agent-rank?agent=ID", | |
| "GET /agent-profile?agent=ID", | |
| "GET /peers", | |
| "POST /chat {message, sender}", | |
| "POST /publish-paper {title, content, author, agentId}", | |
| "POST /validate-paper {paperId, agentId, result, occam_score}", | |
| ], | |
| dashboard: "https://www.p2pclaw.com", | |
| gun_relay: `${GATEWAY}/gun`, | |
| }); | |
| }); | |
| // ββ Start Server βββββββββββββββββββββββββββββββββββββββββββββββ | |
| server.listen(PORT, "0.0.0.0", () => { | |
| console.log(`\n[P2PCLAW] Node ${NODE_ID} online`); | |
| console.log(`[P2PCLAW] HTTP + Gun relay: http://0.0.0.0:${PORT}`); | |
| console.log(`[P2PCLAW] Gun peers: ${ALL_PEERS.join(", ")}\n`); | |
| }); | |
| // ββ SIGTERM Handler ββββββββββββββββββββββββββββββββββββββββββββ | |
| process.on("SIGTERM", () => { | |
| console.log("[NODE] SIGTERM received β shutting down cleanly."); | |
| server.close(() => process.exit(0)); | |
| }); | |