From ff8e51cceeeefedd33daed7705c1505a8dd68b49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:53:46 +0000 Subject: [PATCH 1/3] feat: activate orchestrator/shield/health-checker/innovation modules - Create backend/modules/github-ops.js: GitHub pull/PR/merge/workflow-trigger/rollback ops - Create scripts/system-shield.js: real-time file-watch (chokidar) + PM2 process-monitor + deploy-lock + HTTP API - Enhance scripts/health-guardian.js: add external URL check (https://zeusai.pro) + orchestrator notifications - Enhance autonomous-orchestrator.js: integrate github-ops cycle (pull, CI-check, rollback) - Update ecosystem.config.js: add unicorn-system-shield PM2 process + external health URL env vars - Wire /api/github-ops/* routes into backend/index.js Agent-Logs-Url: https://github.com/ruffy80/ZeusAI/sessions/d8f4af31-6da5-4108-8ec4-4b73cafe0e53 Co-authored-by: ruffy80 <29306714+ruffy80@users.noreply.github.com> --- UNICORN_FINAL/autonomous-orchestrator.js | 69 +++- UNICORN_FINAL/backend/index.js | 35 ++ UNICORN_FINAL/backend/modules/github-ops.js | 296 +++++++++++++++ UNICORN_FINAL/ecosystem.config.js | 36 +- UNICORN_FINAL/scripts/health-guardian.js | 175 ++++++--- UNICORN_FINAL/scripts/system-shield.js | 376 ++++++++++++++++++++ 6 files changed, 926 insertions(+), 61 deletions(-) create mode 100644 UNICORN_FINAL/backend/modules/github-ops.js create mode 100644 UNICORN_FINAL/scripts/system-shield.js diff --git a/UNICORN_FINAL/autonomous-orchestrator.js b/UNICORN_FINAL/autonomous-orchestrator.js index ce2f09b1..81d6ad19 100644 --- a/UNICORN_FINAL/autonomous-orchestrator.js +++ b/UNICORN_FINAL/autonomous-orchestrator.js @@ -24,8 +24,10 @@ const DECISION_INTERVAL = parseInt(process.env.DECISION_INTERVAL || '45' const HEALTH_INTERVAL = 15 * 1000; const BACKEND_HEAL_CMD = process.env.BACKEND_HEAL_CMD || ''; const EDGE_HEALTH_URL = process.env.EDGE_HEALTH_URL || process.env.PUBLIC_APP_URL || ''; +const GH_PULL_INTERVAL = parseInt(process.env.GH_PULL_INTERVAL || '600', 10) * 1000; // 10 min +const GH_BRANCH = process.env.GITHUB_DEFAULT_BRANCH || 'main'; -let innovationEngine, revenueEngine, viralEngine, controlPlaneAgent, profitControlLoop; +let innovationEngine, revenueEngine, viralEngine, controlPlaneAgent, profitControlLoop, githubOps; // ─── Load engines gracefully (modules export singleton instances) ──────────── try { @@ -63,6 +65,13 @@ try { console.warn('⚠️ [ORCHESTRATOR] Profit Control Loop unavailable:', e.message); } +try { + githubOps = require(path.join(BASE, 'github-ops')); + console.log('🔧 [ORCHESTRATOR] GitHub Ops loaded'); +} catch (e) { + console.warn('⚠️ [ORCHESTRATOR] GitHub Ops unavailable:', e.message); +} + // ─── Stats tracking ────────────────────────────────────────────────────────── const stats = { startTime: new Date(), @@ -74,6 +83,7 @@ const stats = { monitorCycles: 0, decisionCycles: 0, healthChecks: 0, + githubCycles: 0, errors: 0, }; @@ -305,7 +315,55 @@ async function runDecisionCycle() { } } -// ─── Health report ──────────────────────────────────────────────────────────── +// ─── GitHub Operations cycle ────────────────────────────────────────────────── +async function runGithubCycle() { + stats.githubCycles++; + log('🔧', `GitHub Ops cycle #${stats.githubCycles}`); + if (!githubOps) { + log('⚠️', 'GitHub Ops not loaded — skipping'); + return; + } + try { + const status = githubOps.getStatus(); + if (!status.configured) { + log('⚠️', 'GitHub Ops not configured (GITHUB_TOKEN/GITHUB_REPOSITORY missing)'); + return; + } + + // 1. Pull latest from main branch + try { + const pullResult = await githubOps.pullLatest(GH_BRANCH); + log('⬇️', `git pull ${GH_BRANCH}`, { summary: pullResult.summary }); + } catch (pullErr) { + log('⚠️', `git pull failed: ${pullErr.message}`); + } + + // 2. Check if last CI workflow run failed → consider rollback + try { + const runs = await githubOps.getWorkflowRuns('deploy-hetzner.yml', 3); + const latestRun = runs[0]; + if (latestRun && latestRun.conclusion === 'failure') { + log('🚨', 'Last deploy workflow FAILED — checking if rollback needed', { run: latestRun.id, sha: latestRun.sha }); + const GH_AUTO_ROLLBACK = String(process.env.GH_AUTO_ROLLBACK || 'false').toLowerCase() === 'true'; + if (GH_AUTO_ROLLBACK) { + log('⏪', `Auto-rollback enabled — reverting ${latestRun.sha}`); + await githubOps.rollback(latestRun.sha, GH_BRANCH); + } + } else if (latestRun) { + log('✅', `Latest workflow run: ${latestRun.status}/${latestRun.conclusion || 'pending'}`, { id: latestRun.id }); + } + } catch (ciErr) { + log('⚠️', `CI status check failed: ${ciErr.message}`); + } + + log('🔧', 'GitHub Ops cycle complete', githubOps.getStatus().ops); + } catch (e) { + stats.errors++; + log('❌', 'GitHub Ops cycle error:', e.message); + } +} + + function printHealthReport() { stats.healthChecks++; const report = { @@ -318,12 +376,14 @@ function printHealthReport() { monitorCycles: stats.monitorCycles, decisionCycles: stats.decisionCycles, healthChecks: stats.healthChecks, + githubCycles: stats.githubCycles, errors: stats.errors, innovationEngine: innovationEngine ? 'ACTIVE' : 'MOCKED', revenueEngine: revenueEngine ? 'ACTIVE' : 'MOCKED', viralEngine: viralEngine ? 'ACTIVE' : 'MOCKED', controlPlaneAgent: controlPlaneAgent ? 'ACTIVE' : 'MOCKED', profitControlLoop: profitControlLoop ? 'ACTIVE' : 'MOCKED', + githubOps: githubOps ? 'ACTIVE' : 'UNAVAILABLE', nextInnovation: `${Math.round(INNOVATION_INTERVAL / 1000)}s`, nextRevenue: `${Math.round(REVENUE_INTERVAL / 1000)}s`, nextViral: `${Math.round(VIRAL_INTERVAL / 1000)}s`, @@ -354,6 +414,7 @@ process.on('uncaughtException', (err) => { log('🦄', ` Platform check every ${PLATFORM_INTERVAL / 1000}s`); log('🦄', ` Auto-Monitor every ${MONITOR_INTERVAL / 1000}s`); log('🦄', ` Auto-Decision AI every ${DECISION_INTERVAL / 1000}s`); + log('🦄', ` GitHub pull/rollback every ${GH_PULL_INTERVAL / 1000}s`); log('🦄', '══════════════════════════════════════════'); // Immediate first cycles @@ -364,6 +425,7 @@ process.on('uncaughtException', (err) => { await runPlatformCycle(); await runMonitorCycle(); await runDecisionCycle(); + await runGithubCycle(); printHealthReport(); // Schedule recurring cycles @@ -374,7 +436,8 @@ process.on('uncaughtException', (err) => { setInterval(runPlatformCycle, PLATFORM_INTERVAL); setInterval(runMonitorCycle, MONITOR_INTERVAL); setInterval(runDecisionCycle, DECISION_INTERVAL); + setInterval(runGithubCycle, GH_PULL_INTERVAL); setInterval(printHealthReport, HEALTH_INTERVAL); - log('✅', 'All 8 autonomous cycles scheduled and running 🚀'); + log('✅', 'All 9 autonomous cycles scheduled and running 🚀'); })(); diff --git a/UNICORN_FINAL/backend/index.js b/UNICORN_FINAL/backend/index.js index e0639cf7..32fcd36c 100644 --- a/UNICORN_FINAL/backend/index.js +++ b/UNICORN_FINAL/backend/index.js @@ -462,6 +462,7 @@ const profitLoop = require('./modules/profit-control-loop'); const centralOrchestrator = require('./modules/central-orchestrator'); const selfHealingEngine = require('./modules/self-healing-engine'); const autoInnovationLoop = require('./modules/auto-innovation-loop'); +const githubOps = require('./modules/github-ops'); // ==================== DYNAMIC PRICING ENGINE ==================== const dynamicPricing = require('./modules/dynamic-pricing'); @@ -3286,6 +3287,40 @@ app.post('/api/innovation-loop/trigger', adminTokenMiddleware, async (req, res) } catch (e) { res.status(500).json({ error: e.message }); } }); +// ==================== GITHUB OPS ROUTES ==================== +app.get('/api/github-ops/status', (req, res) => { + res.json(githubOps.getStatus()); +}); +app.get('/api/github-ops/workflow-runs', adminTokenMiddleware, async (req, res) => { + try { + const workflowId = req.query.workflow || 'deploy-hetzner.yml'; + const limit = Math.min(parseInt(req.query.limit || '10', 10), 50); + const runs = await githubOps.getWorkflowRuns(workflowId, limit); + res.json({ workflowId, runs }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); +app.post('/api/github-ops/pull', adminTokenMiddleware, async (req, res) => { + try { + const branch = req.body.branch || process.env.GITHUB_DEFAULT_BRANCH || 'main'; + const result = await githubOps.pullLatest(branch); + res.json({ success: true, branch, summary: result.summary }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); +app.post('/api/github-ops/trigger-workflow', adminTokenMiddleware, async (req, res) => { + try { + const { workflowId = 'deploy-hetzner.yml', branch, inputs } = req.body; + const result = await githubOps.triggerWorkflow(workflowId, branch, inputs); + res.json(result); + } catch (e) { res.status(500).json({ error: e.message }); } +}); +app.post('/api/github-ops/rollback', adminTokenMiddleware, async (req, res) => { + try { + const { commitSha, branch } = req.body; + const result = await githubOps.rollback(commitSha, branch); + res.json(result); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + // ==================== SELF CONSTRUCTION & TOTAL SYSTEM HEALER ROUTES ==================== app.get('/api/self-construction/status', adminTokenMiddleware, (req, res) => { res.json({ module: 'SelfConstruction', status: 'active', hasRun: selfConstruction.hasRun }); diff --git a/UNICORN_FINAL/backend/modules/github-ops.js b/UNICORN_FINAL/backend/modules/github-ops.js new file mode 100644 index 00000000..e840fe32 --- /dev/null +++ b/UNICORN_FINAL/backend/modules/github-ops.js @@ -0,0 +1,296 @@ +// ===================================================================== +// OWNERSHIP: Acest fișier este proprietatea exclusivă a lui Vladoi Ionut +// Email: vladoi_ionut@yahoo.com +// BTC Address: bc1q4f7e66z87mdfj56kz0dj5hvcnpmh0qh4wuv22e +// Data: 2026-04-13T19:44:00.000Z +// Orice copiere, modificare sau distribuție neautorizată este interzisă. +// ===================================================================== + +/** + * GITHUB OPS — Orchestrator GitHub Integration + * + * Oferă operații GitHub pentru orchestrator: + * 1. pullLatest(branch) — git pull origin + * 2. createBranch(name, from) — crează un branch nou + * 3. createPR(opts) — deschide un Pull Request via GitHub API + * 4. mergePR(number, method) — merge PR via GitHub API + * 5. triggerWorkflow(id,branch) — declanșează GitHub Actions workflow (CI/teste) + * 6. getWorkflowRuns(id,limit) — citește ultimele rulări ale unui workflow + * 7. rollback(commitSha) — revert commit + push (sau force-reset) + * 8. getStatus() — returnează starea modulului + * + * Token: GITHUB_TOKEN (PAT cu scope repo + workflow) + * Repo: GITHUB_REPOSITORY (owner/repo) sau GITHUB_REPO_OWNER + GITHUB_REPO_NAME + */ + +'use strict'; + +const path = require('path'); +const crypto = require('crypto'); + +// Lazy-load simple-git pentru a evita erori dacă pachetul lipseşte +let _git = null; +function getGit() { + if (_git) return _git; + try { + const simpleGit = require('simple-git'); + const rootPath = path.join(__dirname, '..', '..'); // UNICORN_FINAL root + _git = simpleGit(rootPath, { binary: 'git' }); + return _git; + } catch { + return null; + } +} + +// ─── Config ────────────────────────────────────────────────────────────────── +function getToken() { return process.env.GITHUB_TOKEN || ''; } +function getOwner() { return process.env.GITHUB_REPO_OWNER || (getRepo().split('/')[0] || ''); } +function getName() { return process.env.GITHUB_REPO_NAME || (getRepo().split('/')[1] || ''); } +function getRepo() { return process.env.GITHUB_REPOSITORY || ''; } +const DEFAULT_BRANCH = process.env.GITHUB_DEFAULT_BRANCH || 'main'; +const API_HOST = 'api.github.com'; +const MAX_LOG = 200; + +// ─── State ─────────────────────────────────────────────────────────────────── +const _log = []; +const _ops = { pull: 0, createPR: 0, mergePR: 0, workflow: 0, rollback: 0, errors: 0 }; +const startedAt = new Date().toISOString(); + +function _addLog(type, msg, extra) { + const entry = { ts: new Date().toISOString(), type, msg }; + if (extra) entry.extra = typeof extra === 'string' ? extra.slice(0, 300) : extra; + _log.push(entry); + if (_log.length > MAX_LOG) _log.shift(); + console.log(`[GithubOps] ${entry.ts} [${type}] ${msg}`, extra || ''); +} + +// ─── GitHub API helper ──────────────────────────────────────────────────────── +function githubAPI(method, path, body) { + return new Promise((resolve, reject) => { + const token = getToken(); + if (!token) return reject(new Error('GITHUB_TOKEN not configured')); + + const payload = body ? JSON.stringify(body) : null; + const https = require('https'); + const opts = { + hostname: API_HOST, + path, + method, + headers: { + 'User-Agent': 'unicorn-github-ops/1.0', + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + }, + timeout: 15000, + }; + if (payload) { + opts.headers['Content-Type'] = 'application/json'; + opts.headers['Content-Length'] = Buffer.byteLength(payload); + } + + const req = https.request(opts, (res) => { + let data = ''; + res.on('data', (c) => { data += c; }); + res.on('end', () => { + try { + const json = data ? JSON.parse(data) : {}; + if (res.statusCode >= 400) { + return reject(Object.assign(new Error(`GitHub API ${res.statusCode}: ${json.message || data.slice(0, 100)}`), { statusCode: res.statusCode, json })); + } + resolve({ status: res.statusCode, data: json }); + } catch { + resolve({ status: res.statusCode, data }); + } + }); + }); + + req.on('error', reject); + req.on('timeout', () => { req.destroy(new Error('GitHub API timeout')); }); + if (payload) req.write(payload); + req.end(); + }); +} + +// ─── Operations ────────────────────────────────────────────────────────────── + +/** + * git pull origin + */ +async function pullLatest(branch = DEFAULT_BRANCH) { + const git = getGit(); + if (!git) throw new Error('simple-git not available'); + _addLog('PULL', `pulling origin/${branch}`); + const token = getToken(); + const owner = getOwner(); + const name = getName(); + if (token && owner && name) { + try { + const remote = `https://x-access-token:${encodeURIComponent(token)}@github.com/${owner}/${name}.git`; + await git.remote(['set-url', 'origin', remote]); + } catch { /* ignore */ } + } + const result = await git.pull('origin', branch, { '--rebase': 'false' }); + _ops.pull++; + _addLog('PULL_OK', `pulled ${branch}`, result.summary); + return result; +} + +/** + * Crează un branch local + push la origin + */ +async function createBranch(branchName, fromBranch = DEFAULT_BRANCH) { + const git = getGit(); + if (!git) throw new Error('simple-git not available'); + _addLog('CREATE_BRANCH', `${fromBranch} → ${branchName}`); + await git.checkoutBranch(branchName, `origin/${fromBranch}`); + await git.push('origin', branchName, { '--set-upstream': null }); + _addLog('CREATE_BRANCH_OK', branchName); + return { branch: branchName, from: fromBranch }; +} + +/** + * Deschide un Pull Request via GitHub API + * @param {{ title, body, head, base }} opts + */ +async function createPR({ title, body = '', head, base = DEFAULT_BRANCH }) { + const owner = getOwner(); + const repo = getName(); + if (!owner || !repo) throw new Error('GITHUB_REPOSITORY not configured'); + _addLog('CREATE_PR', `${head} → ${base}: ${title}`); + const res = await githubAPI('POST', `/repos/${owner}/${repo}/pulls`, { title, body, head, base, draft: false }); + _ops.createPR++; + _addLog('CREATE_PR_OK', `PR #${res.data.number}`, res.data.html_url); + return res.data; +} + +/** + * Merge un Pull Request via GitHub API + */ +async function mergePR(number, mergeMethod = 'squash') { + const owner = getOwner(); + const repo = getName(); + if (!owner || !repo) throw new Error('GITHUB_REPOSITORY not configured'); + _addLog('MERGE_PR', `#${number} method=${mergeMethod}`); + const res = await githubAPI('PUT', `/repos/${owner}/${repo}/pulls/${number}/merge`, { + merge_method: mergeMethod, + commit_title: `[Orchestrator] Auto-merge PR #${number}`, + commit_message: `Merged automatically by Unicorn Orchestrator at ${new Date().toISOString()}`, + }); + _ops.mergePR++; + _addLog('MERGE_PR_OK', `#${number} merged`, res.data.sha); + return res.data; +} + +/** + * Declanșează un GitHub Actions workflow (workflow_dispatch event) + */ +async function triggerWorkflow(workflowId = 'deploy-hetzner.yml', branch = DEFAULT_BRANCH, inputs = {}) { + const owner = getOwner(); + const repo = getName(); + if (!owner || !repo) throw new Error('GITHUB_REPOSITORY not configured'); + _addLog('TRIGGER_WORKFLOW', `${workflowId} @ ${branch}`); + await githubAPI('POST', `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflowId)}/dispatches`, { + ref: branch, + inputs, + }); + _ops.workflow++; + _addLog('TRIGGER_WORKFLOW_OK', `${workflowId} triggered`); + return { triggered: true, workflow: workflowId, branch }; +} + +/** + * Citește ultimele rulări ale unui workflow + */ +async function getWorkflowRuns(workflowId = 'deploy-hetzner.yml', limit = 5) { + const owner = getOwner(); + const repo = getName(); + if (!owner || !repo) throw new Error('GITHUB_REPOSITORY not configured'); + const res = await githubAPI('GET', `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflowId)}/runs?per_page=${limit}`); + return (res.data.workflow_runs || []).map((r) => ({ + id: r.id, + status: r.status, + conclusion: r.conclusion, + createdAt: r.created_at, + branch: r.head_branch, + sha: r.head_sha, + url: r.html_url, + })); +} + +/** + * Rollback: revert la ultimul commit bun sau la un SHA specific + * Strategia 1: git revert --no-edit + push + * Strategia 2: dacă nu există sha — revine la HEAD~1 + */ +async function rollback(commitSha = null, branch = DEFAULT_BRANCH) { + const git = getGit(); + if (!git) throw new Error('simple-git not available'); + + // Actualizăm remote cu token pentru push autentificat + const token = getToken(); + const owner = getOwner(); + const name = getName(); + if (token && owner && name) { + try { + const remote = `https://x-access-token:${encodeURIComponent(token)}@github.com/${owner}/${name}.git`; + await git.remote(['set-url', 'origin', remote]); + } catch { /* ignore */ } + } + + const targetSha = commitSha || 'HEAD'; + _addLog('ROLLBACK', `reverting ${targetSha} on ${branch}`); + _ops.rollback++; + + try { + // Checkout branch şi pull latest + await git.checkout(branch); + await git.pull('origin', branch, { '--rebase': 'false' }); + // Revert commit + await git.revert([targetSha, '--no-edit']); + // Push rollback commit + await git.push('origin', branch); + _addLog('ROLLBACK_OK', `reverted ${targetSha}, pushed to ${branch}`); + return { ok: true, reverted: targetSha, branch }; + } catch (err) { + _ops.errors++; + _addLog('ROLLBACK_ERR', err.message); + throw err; + } +} + +/** + * Verifică dacă ultimul workflow run a eşuat şi returnează SHA-ul commit-ului + */ +async function getLastFailedCommit(workflowId = 'deploy-hetzner.yml') { + const runs = await getWorkflowRuns(workflowId, 3); + const failed = runs.find((r) => r.conclusion === 'failure'); + return failed || null; +} + +/** + * Returnează starea modulului + */ +function getStatus() { + return { + name: 'GithubOps', + startedAt, + configured: !!(getToken() && getOwner() && getName()), + owner: getOwner() || null, + repo: getName() || null, + ops: { ... _ops }, + recentLog: _log.slice(-20), + }; +} + +module.exports = { + pullLatest, + createBranch, + createPR, + mergePR, + triggerWorkflow, + getWorkflowRuns, + rollback, + getLastFailedCommit, + getStatus, +}; diff --git a/UNICORN_FINAL/ecosystem.config.js b/UNICORN_FINAL/ecosystem.config.js index 8d18877b..207c8f28 100644 --- a/UNICORN_FINAL/ecosystem.config.js +++ b/UNICORN_FINAL/ecosystem.config.js @@ -63,6 +63,8 @@ module.exports = { REVENUE_INTERVAL: '15', VIRAL_INTERVAL: '20', DEPLOYMENT_INTERVAL: '120', + GH_PULL_INTERVAL: '600', + GH_AUTO_ROLLBACK: 'false', BACKEND_HEAL_CMD: 'pm2 restart unicorn', BACKEND_BASE_URL: 'http://127.0.0.1:3000', DOMAIN: SITE_DOMAIN, @@ -88,7 +90,10 @@ module.exports = { NODE_ENV: 'production', HEALTH_GUARDIAN_URL: 'http://127.0.0.1:3000/api/health', HEALTH_GUARDIAN_SHIELD_URL: 'http://127.0.0.1:3000/api/quantum-integrity/status', + HEALTH_GUARDIAN_EXTERNAL_URL: `${PUBLIC_APP_URL}/health`, + HEALTH_GUARDIAN_ORCHESTRATOR_URL: 'http://127.0.0.1:3000/api/orchestrator/check', HEALTH_GUARDIAN_INTERVAL_MS: '30000', + HEALTH_GUARDIAN_EXTERNAL_MS: '120000', HEALTH_GUARDIAN_FAIL_THRESHOLD: '3', HEALTH_GUARDIAN_HEAL_CMD: `pm2 startOrRestart "${ECOSYSTEM_PATH}" --only unicorn,unicorn-orchestrator,unicorn-health-guardian,unicorn-quantum-watchdog`, HEALTH_GUARDIAN_ROLLBACK_CMD: 'bash scripts/rollback-last-backup.sh' @@ -170,8 +175,35 @@ module.exports = { log_date_format: 'YYYY-MM-DD HH:mm:ss' }, - // ── 6. Llama Bridge (starts Ollama serve at P4 / nice +10) ──────────────── - // Prerequisite: `ollama` must be installed on the server. + // ── 5b. System Shield (real-time file watch + process monitor + deploy lock) ─ + { + name: 'unicorn-system-shield', + script: 'scripts/system-shield.js', + cwd: __dirname, + instances: 1, + autorestart: true, + watch: false, + max_restarts: 20, + min_uptime: '10s', + restart_delay: 5000, + env: { + NODE_ENV: 'production', + SHIELD_PORT: '3099', + SHIELD_PROCESS_POLL: '30000', + SHIELD_FILE_RESTORE: 'true', + SHIELD_HEAL_COOLDOWN_MS: '60000', + SHIELD_REQUIRED_PROCS: 'unicorn,unicorn-orchestrator,unicorn-health-guardian,unicorn-quantum-watchdog,unicorn-platform-connector,unicorn-uaic', + SHIELD_NOTIFY_URL: 'http://127.0.0.1:3000/api/orchestrator/check', + SHIELD_LOCK_FILE: '/tmp/unicorn-deploy.lock', + DOMAIN: SITE_DOMAIN, + PUBLIC_APP_URL + }, + error_file: 'logs/system-shield-error.log', + out_file: 'logs/system-shield-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss' + }, + + // ── 6. Llama Bridge (starts Ollama serve at P4 / nice +10) ──────────────── // Prerequisite: `ollama` must be installed on the server. // Install: curl -fsSL https://ollama.ai/install.sh | sh // Pull model: ollama pull llama3.1:8b-instruct-q4_K_M { diff --git a/UNICORN_FINAL/scripts/health-guardian.js b/UNICORN_FINAL/scripts/health-guardian.js index 71684ee0..2c2875f4 100644 --- a/UNICORN_FINAL/scripts/health-guardian.js +++ b/UNICORN_FINAL/scripts/health-guardian.js @@ -1,18 +1,25 @@ #!/usr/bin/env node 'use strict'; -const http = require('http'); +const http = require('http'); +const https = require('https'); const { exec } = require('child_process'); -const CHECK_URL = process.env.HEALTH_GUARDIAN_URL || 'http://127.0.0.1:3000/api/health'; -const SHIELD_URL = process.env.HEALTH_GUARDIAN_SHIELD_URL || 'http://127.0.0.1:3000/api/quantum-integrity/status'; -const CHECK_INTERVAL_MS = Math.max(parseInt(process.env.HEALTH_GUARDIAN_INTERVAL_MS || '60000', 10), 10000); -const FAIL_THRESHOLD = Math.max(parseInt(process.env.HEALTH_GUARDIAN_FAIL_THRESHOLD || '3', 10), 1); -const HEAL_CMD = process.env.HEALTH_GUARDIAN_HEAL_CMD || 'systemctl restart unicorn'; -const ROLLBACK_CMD = process.env.HEALTH_GUARDIAN_ROLLBACK_CMD || ''; +const CHECK_URL = process.env.HEALTH_GUARDIAN_URL || 'http://127.0.0.1:3000/api/health'; +const SHIELD_URL = process.env.HEALTH_GUARDIAN_SHIELD_URL || 'http://127.0.0.1:3000/api/quantum-integrity/status'; +// URL extern public (ex. https://zeusai.pro sau https://zeusai.pro/health) +const EXTERNAL_URL = process.env.HEALTH_GUARDIAN_EXTERNAL_URL || process.env.PUBLIC_APP_URL || process.env.EDGE_HEALTH_URL || ''; +// URL orchestrator pentru notificări (POST JSON) +const ORCHESTRATOR_URL = process.env.HEALTH_GUARDIAN_ORCHESTRATOR_URL || 'http://127.0.0.1:3000/api/orchestrator/check'; +const CHECK_INTERVAL_MS = Math.max(parseInt(process.env.HEALTH_GUARDIAN_INTERVAL_MS || '60000', 10), 10000); +const EXTERNAL_INTERVAL_MS = Math.max(parseInt(process.env.HEALTH_GUARDIAN_EXTERNAL_MS || '120000', 10), 30000); +const FAIL_THRESHOLD = Math.max(parseInt(process.env.HEALTH_GUARDIAN_FAIL_THRESHOLD || '3', 10), 1); +const HEAL_CMD = process.env.HEALTH_GUARDIAN_HEAL_CMD || 'systemctl restart unicorn'; +const ROLLBACK_CMD = process.env.HEALTH_GUARDIAN_ROLLBACK_CMD || ''; -let consecutiveFailures = 0; +let consecutiveFailures = 0; let consecutiveIntegrityFailures = 0; +let consecutiveExternalFailures = 0; function log(msg, extra) { const ts = new Date().toISOString(); @@ -54,36 +61,61 @@ function checkHealth() { }); } -function checkShield() { +/** Verifică URL-ul public extern (https://zeusai.pro) */ +function checkExternal() { + if (!EXTERNAL_URL) return Promise.resolve({ ok: true, skipped: true }); return new Promise((resolve) => { - const req = http.get(SHIELD_URL, (res) => { + let parsedUrl; + try { parsedUrl = new URL(EXTERNAL_URL); } catch { + return resolve({ ok: false, reason: 'invalid-url' }); + } + const mod = parsedUrl.protocol === 'https:' ? https : http; + const checkPath = (parsedUrl.pathname && parsedUrl.pathname !== '/') ? parsedUrl.pathname : '/health'; + const opts = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), + path: checkPath, + method: 'GET', + timeout: 12000, + headers: { 'User-Agent': 'unicorn-health-guardian/1.0' }, + }; + const req = mod.request(opts, (res) => { let body = ''; - res.on('data', (chunk) => { - body += chunk; - }); + res.on('data', (c) => { body += c; }); res.on('end', () => { - if (res.statusCode !== 200) { - return resolve({ ok: false, reason: `status:${res.statusCode}` }); - } - try { - const json = JSON.parse(body || '{}'); - const integrity = String(json.integrity || '').toLowerCase(); - const ok = integrity === 'intact' || integrity === 'pending'; - resolve({ ok, reason: ok ? 'ok' : integrity || 'unknown' }); - } catch { - resolve({ ok: false, reason: 'invalid-json' }); + if (res.statusCode >= 200 && res.statusCode < 400) { + return resolve({ ok: true, status: res.statusCode }); } + resolve({ ok: false, reason: `external:status:${res.statusCode}`, body: body.slice(0, 100) }); }); }); - - req.on('error', (err) => resolve({ ok: false, reason: err.message })); - req.setTimeout(8000, () => { - req.destroy(new Error('timeout')); - resolve({ ok: false, reason: 'timeout' }); - }); + req.on('error', (err) => resolve({ ok: false, reason: `external:${err.message}` })); + req.on('timeout', () => { req.destroy(); resolve({ ok: false, reason: 'external:timeout' }); }); + req.end(); }); } +/** Notifică orchestratorul cu un event */ +function notifyOrchestrator(event, details) { + if (!ORCHESTRATOR_URL) return; + const payload = JSON.stringify({ source: 'health-guardian', event, details, ts: new Date().toISOString() }); + try { + const url = new URL(ORCHESTRATOR_URL); + const mod = url.protocol === 'https:' ? https : http; + const req = mod.request({ + hostname: url.hostname, + port: url.port || (url.protocol === 'https:' ? 443 : 80), + path: url.pathname, + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }, + timeout: 5000, + }, (res) => { res.resume(); }); + req.on('error', () => { /* silently ignore notification failures */ }); + req.write(payload); + req.end(); + } catch { /* ignore */ } +} + function heal() { return new Promise((resolve) => { exec(HEAL_CMD, (err, stdout, stderr) => { @@ -102,18 +134,19 @@ async function loop() { if (result.ok) { if (consecutiveFailures > 0) { log(`health restored after ${consecutiveFailures} failure(s)`); + notifyOrchestrator('health_restored', { url: CHECK_URL }); } consecutiveFailures = 0; - return; - } - - consecutiveFailures += 1; - log(`health check failed (${consecutiveFailures}/${FAIL_THRESHOLD})`, result.reason || result.body || 'unknown'); + } else { + consecutiveFailures += 1; + log(`health check failed (${consecutiveFailures}/${FAIL_THRESHOLD})`, result.reason || result.body || 'unknown'); + notifyOrchestrator('health_failure', { url: CHECK_URL, reason: result.reason, failures: consecutiveFailures }); - if (consecutiveFailures >= FAIL_THRESHOLD) { - log('triggering auto-heal...'); - await heal(); - consecutiveFailures = 0; + if (consecutiveFailures >= FAIL_THRESHOLD) { + log('triggering auto-heal...'); + await heal(); + consecutiveFailures = 0; + } } if (shield.ok) { @@ -121,30 +154,60 @@ async function loop() { log(`integrity restored after ${consecutiveIntegrityFailures} failure(s)`); } consecutiveIntegrityFailures = 0; + } else { + consecutiveIntegrityFailures += 1; + log(`integrity check failed (${consecutiveIntegrityFailures}/${FAIL_THRESHOLD})`, shield.reason || 'unknown'); + notifyOrchestrator('integrity_failure', { reason: shield.reason, failures: consecutiveIntegrityFailures }); + if (consecutiveIntegrityFailures >= FAIL_THRESHOLD) { + log('triggering integrity heal...'); + await heal(); + if (ROLLBACK_CMD) { + await new Promise((resolve) => { + exec(ROLLBACK_CMD, (err, stdout, stderr) => { + if (err) { + log('rollback command failed', { error: err.message, stderr: (stderr || '').slice(0, 250) }); + } else { + log('rollback command executed', { stdout: (stdout || '').slice(0, 250) }); + } + resolve(); + }); + }); + } + consecutiveIntegrityFailures = 0; + } + } +} + +async function externalLoop() { + const result = await checkExternal(); + if (result.skipped) return; + + if (result.ok) { + if (consecutiveExternalFailures > 0) { + log(`external health restored after ${consecutiveExternalFailures} failure(s)`, { url: EXTERNAL_URL }); + notifyOrchestrator('external_restored', { url: EXTERNAL_URL }); + } + consecutiveExternalFailures = 0; return; } - consecutiveIntegrityFailures += 1; - log(`integrity check failed (${consecutiveIntegrityFailures}/${FAIL_THRESHOLD})`, shield.reason || 'unknown'); - if (consecutiveIntegrityFailures >= FAIL_THRESHOLD) { - log('triggering integrity heal...'); + consecutiveExternalFailures += 1; + log(`external health check failed (${consecutiveExternalFailures}/${FAIL_THRESHOLD})`, { url: EXTERNAL_URL, reason: result.reason }); + notifyOrchestrator('external_failure', { url: EXTERNAL_URL, reason: result.reason, failures: consecutiveExternalFailures }); + + if (consecutiveExternalFailures >= FAIL_THRESHOLD) { + log('external down threshold reached — triggering heal', { url: EXTERNAL_URL }); await heal(); - if (ROLLBACK_CMD) { - await new Promise((resolve) => { - exec(ROLLBACK_CMD, (err, stdout, stderr) => { - if (err) { - log('rollback command failed', { error: err.message, stderr: (stderr || '').slice(0, 250) }); - } else { - log('rollback command executed', { stdout: (stdout || '').slice(0, 250) }); - } - resolve(); - }); - }); - } - consecutiveIntegrityFailures = 0; + consecutiveExternalFailures = 0; } } -log(`started. interval=${CHECK_INTERVAL_MS}ms, threshold=${FAIL_THRESHOLD}, url=${CHECK_URL}, shield=${SHIELD_URL}`); +const externalInfo = EXTERNAL_URL ? `, external=${EXTERNAL_URL}` : ' (no external URL configured)'; +log(`started. interval=${CHECK_INTERVAL_MS}ms, threshold=${FAIL_THRESHOLD}, url=${CHECK_URL}, shield=${SHIELD_URL}${externalInfo}`); setInterval(loop, CHECK_INTERVAL_MS); loop(); + +if (EXTERNAL_URL) { + setInterval(externalLoop, EXTERNAL_INTERVAL_MS); + setTimeout(externalLoop, 10000); // primă verificare externă după 10s +} diff --git a/UNICORN_FINAL/scripts/system-shield.js b/UNICORN_FINAL/scripts/system-shield.js new file mode 100644 index 00000000..04c44c7f --- /dev/null +++ b/UNICORN_FINAL/scripts/system-shield.js @@ -0,0 +1,376 @@ +#!/usr/bin/env node +// ===================================================================== +// OWNERSHIP: Acest fișier este proprietatea exclusivă a lui Vladoi Ionut +// Email: vladoi_ionut@yahoo.com +// BTC Address: bc1q4f7e66z87mdfj56kz0dj5hvcnpmh0qh4wuv22e +// Data: 2026-04-13T19:44:00.000Z +// Orice copiere, modificare sau distribuție neautorizată este interzisă. +// ===================================================================== + +/** + * SYSTEM SHIELD — Scut autonom pentru fișiere, procese şi deploy-uri + * + * Responsabilități: + * 1. Monitorizare fișiere critice în timp real (chokidar fs.watch) + * 2. Monitorizare procese PM2 la fiecare PROCESS_POLL_MS + * 3. Reparare fișiere corupte/lipsă din git (git checkout -- ) + * 4. Repornire procese căzute via pm2 restart + * 5. Deploy-lock: blochează deploy-uri incomplete (lock file) + * 6. HTTP API pe portul SHIELD_PORT (/status, /lock, /unlock, /heal) + * + * Env vars: + * SHIELD_PORT — portul HTTP intern (default: 3099) + * SHIELD_PROCESS_POLL — interval poll procese ms (default: 30000) + * SHIELD_FILE_RESTORE — 'true' să restaureze din git (default: true) + * SHIELD_REQUIRED_PROCS — procese PM2 obligatorii (csv, default: unicorn,unicorn-orchestrator,unicorn-health-guardian) + * SHIELD_CRITICAL_FILES — fișiere critice de monitorizat (csv) + * SHIELD_LOCK_FILE — calea lock-ului de deploy (default: /tmp/unicorn-deploy.lock) + * SHIELD_NOTIFY_URL — URL orchestrator pentru notificări (POST) + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const { exec, execSync } = require('child_process'); +const http = require('http'); + +// ─── Config ─────────────────────────────────────────────────────────────────── +const ROOT = path.join(__dirname, '..'); // UNICORN_FINAL/ +const SHIELD_PORT = parseInt(process.env.SHIELD_PORT || '3099', 10); +const PROCESS_POLL_MS = Math.max(parseInt(process.env.SHIELD_PROCESS_POLL || '30000', 10), 10000); +const FILE_RESTORE = String(process.env.SHIELD_FILE_RESTORE || 'true').toLowerCase() !== 'false'; +const LOCK_FILE = process.env.SHIELD_LOCK_FILE || '/tmp/unicorn-deploy.lock'; +const NOTIFY_URL = process.env.SHIELD_NOTIFY_URL || process.env.ORCHESTRATOR_NOTIFY_URL || ''; +const MAX_LOG = 300; +const HEAL_COOLDOWN_MS = parseInt(process.env.SHIELD_HEAL_COOLDOWN_MS || '60000', 10); + +const REQUIRED_PROCS = (process.env.SHIELD_REQUIRED_PROCS || + 'unicorn,unicorn-orchestrator,unicorn-health-guardian,unicorn-quantum-watchdog,unicorn-platform-connector') + .split(',').map((s) => s.trim()).filter(Boolean); + +const DEFAULT_CRITICAL_FILES = [ + path.join(ROOT, 'backend/index.js'), + path.join(ROOT, 'ecosystem.config.js'), + path.join(ROOT, 'package.json'), + path.join(ROOT, 'scripts/health-guardian.js'), + path.join(ROOT, 'scripts/system-shield.js'), + path.join(ROOT, 'scripts/rollback-last-backup.sh'), + path.join(ROOT, 'autonomous-orchestrator.js'), +]; + +const CRITICAL_FILES = process.env.SHIELD_CRITICAL_FILES + ? process.env.SHIELD_CRITICAL_FILES.split(',').map((f) => f.trim()) + : DEFAULT_CRITICAL_FILES; + +// ─── State ──────────────────────────────────────────────────────────────────── +const state = { + startedAt: new Date().toISOString(), + watchedFiles: [], + missingFiles: [], + restoredFiles: [], + missingProcs: [], + restarted: [], + deployLocked: false, + deployLockInfo: null, + healCount: 0, + errors: 0, + eventLog: [], +}; + +const lastHealAt = {}; // procName → timestamp + +function log(type, msg, extra) { + const entry = { ts: new Date().toISOString(), type, msg }; + if (extra !== undefined) entry.extra = typeof extra === 'string' ? extra.slice(0, 300) : extra; + state.eventLog.push(entry); + if (state.eventLog.length > MAX_LOG) state.eventLog.shift(); + console.log(`[SystemShield] ${entry.ts} [${type}] ${msg}`, extra !== undefined ? extra : ''); +} + +// ─── Deploy Lock ────────────────────────────────────────────────────────────── + +function lockDeploy(reason = '') { + const info = { lockedAt: new Date().toISOString(), reason, pid: process.pid }; + fs.writeFileSync(LOCK_FILE, JSON.stringify(info, null, 2), 'utf8'); + state.deployLocked = true; + state.deployLockInfo = info; + log('DEPLOY_LOCK', `Deploy blocat: ${reason}`, info); +} + +function unlockDeploy() { + try { fs.unlinkSync(LOCK_FILE); } catch { /* already removed */ } + state.deployLocked = false; + state.deployLockInfo = null; + log('DEPLOY_UNLOCK', 'Deploy deblocat'); +} + +function isDeployLocked() { + try { + if (fs.existsSync(LOCK_FILE)) { + const info = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf8') || '{}'); + state.deployLocked = true; + state.deployLockInfo = info; + // Auto-expire lock după 30 minute + const age = Date.now() - new Date(info.lockedAt || 0).getTime(); + if (age > 30 * 60 * 1000) { + log('DEPLOY_LOCK_EXPIRED', 'Lock expirat automat (> 30 min), deblocare'); + unlockDeploy(); + return false; + } + return true; + } + } catch { /* ignore */ } + state.deployLocked = false; + state.deployLockInfo = null; + return false; +} + +// ─── File Watcher ───────────────────────────────────────────────────────────── + +function startFileWatcher() { + let chokidar; + try { chokidar = require('chokidar'); } catch { chokidar = null; } + + if (!chokidar) { + log('WATCHER_WARN', 'chokidar nu este disponibil, se foloseste polling'); + _startPollingWatcher(); + return; + } + + const watcher = chokidar.watch(CRITICAL_FILES, { + persistent: true, + ignoreInitial: true, + awaitWriteFinish: { stabilityThreshold: 1000, pollInterval: 200 }, + }); + + watcher.on('add', (f) => log('FILE_ADDED', f)); + watcher.on('unlink', (f) => { log('FILE_DELETED', f); _handleMissingFile(f); }); + watcher.on('change', (f) => log('FILE_CHANGED', f)); + watcher.on('error', (e) => { state.errors++; log('WATCHER_ERROR', e.message); }); + + state.watchedFiles = CRITICAL_FILES.slice(); + log('WATCHER_STARTED', `Monitorizez ${CRITICAL_FILES.length} fișiere critice`); +} + +function _startPollingWatcher() { + // Fallback: verifică existenţa fișierelor la fiecare 60s + const knownHashes = {}; + for (const f of CRITICAL_FILES) { + try { knownHashes[f] = fs.statSync(f).mtimeMs; } catch { knownHashes[f] = null; } + } + setInterval(() => { + for (const f of CRITICAL_FILES) { + try { + const mtime = fs.statSync(f).mtimeMs; + if (knownHashes[f] === null) { + log('FILE_RESTORED_EXT', f); + knownHashes[f] = mtime; + } else if (mtime !== knownHashes[f]) { + log('FILE_CHANGED_POLL', f); + knownHashes[f] = mtime; + } + } catch { + if (knownHashes[f] !== null) { + log('FILE_DELETED_POLL', f); + knownHashes[f] = null; + _handleMissingFile(f); + } + } + } + }, 60000); + state.watchedFiles = CRITICAL_FILES.slice(); +} + +function _handleMissingFile(filePath) { + state.missingFiles.push({ file: filePath, at: new Date().toISOString() }); + if (!FILE_RESTORE) { + log('FILE_MISSING', filePath, 'restaurare dezactivata'); + _notify('file_missing', { file: filePath }); + return; + } + _restoreFileFromGit(filePath); +} + +function _restoreFileFromGit(filePath) { + const relPath = path.relative(ROOT, filePath); + log('FILE_RESTORE', `Restaurare din git: ${relPath}`); + exec(`git -C "${ROOT}" checkout -- "${relPath}"`, (err, stdout, stderr) => { + if (err) { + log('FILE_RESTORE_ERR', relPath, err.message); + _notify('file_restore_failed', { file: relPath, error: err.message }); + } else { + log('FILE_RESTORE_OK', relPath); + state.restoredFiles.push({ file: filePath, at: new Date().toISOString() }); + _notify('file_restored', { file: relPath }); + } + }); +} + +// ─── Process Monitor ───────────────────────────────────────────────────────── + +function startProcessMonitor() { + setInterval(_checkProcesses, PROCESS_POLL_MS); + setTimeout(_checkProcesses, 5000); // verificare inițială + log('PROCESS_MONITOR_STARTED', `Poll la fiecare ${PROCESS_POLL_MS}ms, procese: ${REQUIRED_PROCS.join(', ')}`); +} + +function _checkProcesses() { + exec('pm2 jlist', { timeout: 10000 }, (err, stdout) => { + if (err) { + log('PM2_UNAVAILABLE', err.message); + return; + } + let list; + try { list = JSON.parse(stdout || '[]'); } catch { list = []; } + + const online = new Set( + list + .filter((p) => p?.pm2_env?.status === 'online') + .map((p) => String(p.name || '').trim()) + ); + + const missing = REQUIRED_PROCS.filter((name) => !online.has(name)); + state.missingProcs = missing; + + if (missing.length === 0) return; + + log('PROCESSES_DOWN', `Procese căzute: ${missing.join(', ')}`); + for (const procName of missing) _restartProcess(procName); + }); +} + +function _restartProcess(procName) { + const now = Date.now(); + const last = lastHealAt[procName] || 0; + if (now - last < HEAL_COOLDOWN_MS) { + log('HEAL_COOLDOWN', `${procName} — cooldown activ, aştept`); + return; + } + lastHealAt[procName] = now; + state.healCount++; + + log('RESTART_PROC', `pm2 restart ${procName}`); + exec(`pm2 restart "${procName}"`, { timeout: 15000 }, (err, stdout) => { + if (err) { + log('RESTART_ERR', procName, err.message); + _notify('process_restart_failed', { process: procName, error: err.message }); + // Încercare start dacă restart a eşuat + exec(`pm2 startOrRestart ecosystem.config.js --only "${procName}"`, { cwd: ROOT, timeout: 20000 }, (err2) => { + if (err2) log('START_ERR', procName, err2.message); + else { log('START_OK', procName); state.restarted.push({ process: procName, at: new Date().toISOString() }); } + }); + } else { + log('RESTART_OK', procName); + state.restarted.push({ process: procName, at: new Date().toISOString(), out: stdout.slice(0, 100) }); + _notify('process_restarted', { process: procName }); + } + }); +} + +// ─── Notificare Orchestrator ────────────────────────────────────────────────── + +function _notify(event, data) { + if (!NOTIFY_URL) return; + const payload = JSON.stringify({ source: 'system-shield', event, data, ts: new Date().toISOString() }); + try { + const url = new URL(NOTIFY_URL); + const mod = url.protocol === 'https:' ? require('https') : require('http'); + const req = mod.request({ + hostname: url.hostname, + port: url.port || (url.protocol === 'https:' ? 443 : 80), + path: url.pathname, + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }, + timeout: 5000, + }, (res) => { res.resume(); }); + req.on('error', () => { /* silently ignore notification failures */ }); + req.write(payload); + req.end(); + } catch { /* ignore */ } +} + +// ─── HTTP API ───────────────────────────────────────────────────────────────── + +function startAPI() { + const server = http.createServer((req, res) => { + const { method, url } = req; + + if (url === '/status' || url === '/') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ + ...state, + eventLog: state.eventLog.slice(-50), + deployLocked: isDeployLocked(), + })); + } + + if (url === '/lock' && method === 'POST') { + let body = ''; + req.on('data', (c) => { body += c; }); + req.on('end', () => { + let reason = ''; + try { reason = JSON.parse(body).reason || ''; } catch { /* noop */ } + lockDeploy(reason || 'locked via API'); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ locked: true, info: state.deployLockInfo })); + }); + return; + } + + if (url === '/unlock' && method === 'POST') { + unlockDeploy(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ locked: false })); + } + + if (url === '/heal' && method === 'POST') { + log('MANUAL_HEAL', 'Declanşat via API'); + _checkProcesses(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ triggered: true })); + } + + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'not found' })); + }); + + server.listen(SHIELD_PORT, '127.0.0.1', () => { + log('API_STARTED', `Shield HTTP API pe 127.0.0.1:${SHIELD_PORT}`); + }); + + server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + log('API_WARN', `Port ${SHIELD_PORT} ocupat — API HTTP dezactivat`); + } else { + log('API_ERROR', err.message); + } + }); +} + +// ─── Bootstrap ─────────────────────────────────────────────────────────────── + +log('SHIELD_START', `System Shield pornit — PID ${process.pid}`); +log('CONFIG', 'Config', { + port: SHIELD_PORT, + pollMs: PROCESS_POLL_MS, + fileRestore: FILE_RESTORE, + lockFile: LOCK_FILE, + requiredProcs: REQUIRED_PROCS, + criticalFiles: CRITICAL_FILES.length, +}); + +isDeployLocked(); // încarcă starea curentă a lock-ului +startFileWatcher(); +startProcessMonitor(); +startAPI(); + +// Menţine procesul viu +process.on('uncaughtException', (err) => { + state.errors++; + log('UNCAUGHT', err.message, err.stack ? err.stack.slice(0, 300) : ''); +}); +process.on('unhandledRejection', (reason) => { + state.errors++; + log('UNHANDLED_REJECTION', String(reason).slice(0, 200)); +}); From 42fca258931dbce5d3e9c6c4a1e398305aead965 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:57:20 +0000 Subject: [PATCH 2/3] fix: address command injection and tainted-format-string security issues - system-shield.js: use execFile instead of exec for pm2/git commands, add isSafeProcName() + isSafeFilePath() validators, log notification errors - github-ops.js: use separate console.log args to avoid tainted-format-string, fix spread whitespace - health-guardian.js: log orchestrator notification errors Agent-Logs-Url: https://github.com/ruffy80/ZeusAI/sessions/d8f4af31-6da5-4108-8ec4-4b73cafe0e53 Co-authored-by: ruffy80 <29306714+ruffy80@users.noreply.github.com> --- UNICORN_FINAL/backend/modules/github-ops.js | 5 +- UNICORN_FINAL/scripts/health-guardian.js | 2 +- UNICORN_FINAL/scripts/system-shield.js | 60 +++++++++++++++++---- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/UNICORN_FINAL/backend/modules/github-ops.js b/UNICORN_FINAL/backend/modules/github-ops.js index e840fe32..21b0c02a 100644 --- a/UNICORN_FINAL/backend/modules/github-ops.js +++ b/UNICORN_FINAL/backend/modules/github-ops.js @@ -61,7 +61,8 @@ function _addLog(type, msg, extra) { if (extra) entry.extra = typeof extra === 'string' ? extra.slice(0, 300) : extra; _log.push(entry); if (_log.length > MAX_LOG) _log.shift(); - console.log(`[GithubOps] ${entry.ts} [${type}] ${msg}`, extra || ''); + // Use separate args (not template literal format) to avoid tainted-format-string + console.log('[GithubOps]', entry.ts, '[' + String(type) + ']', String(msg), extra || ''); } // ─── GitHub API helper ──────────────────────────────────────────────────────── @@ -278,7 +279,7 @@ function getStatus() { configured: !!(getToken() && getOwner() && getName()), owner: getOwner() || null, repo: getName() || null, - ops: { ... _ops }, + ops: { ..._ops }, recentLog: _log.slice(-20), }; } diff --git a/UNICORN_FINAL/scripts/health-guardian.js b/UNICORN_FINAL/scripts/health-guardian.js index 2c2875f4..b785d170 100644 --- a/UNICORN_FINAL/scripts/health-guardian.js +++ b/UNICORN_FINAL/scripts/health-guardian.js @@ -110,7 +110,7 @@ function notifyOrchestrator(event, details) { headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }, timeout: 5000, }, (res) => { res.resume(); }); - req.on('error', () => { /* silently ignore notification failures */ }); + req.on('error', (e) => { log(`orchestrator notify failed: ${ORCHESTRATOR_URL}: ${e.message}`); }); req.write(payload); req.end(); } catch { /* ignore */ } diff --git a/UNICORN_FINAL/scripts/system-shield.js b/UNICORN_FINAL/scripts/system-shield.js index 04c44c7f..ec370899 100644 --- a/UNICORN_FINAL/scripts/system-shield.js +++ b/UNICORN_FINAL/scripts/system-shield.js @@ -85,10 +85,23 @@ function log(type, msg, extra) { if (extra !== undefined) entry.extra = typeof extra === 'string' ? extra.slice(0, 300) : extra; state.eventLog.push(entry); if (state.eventLog.length > MAX_LOG) state.eventLog.shift(); - console.log(`[SystemShield] ${entry.ts} [${type}] ${msg}`, extra !== undefined ? extra : ''); + // Use separate args (not template literal format) to avoid tainted-format-string + console.log('[SystemShield]', entry.ts, '[' + String(type) + ']', String(msg), extra !== undefined ? extra : ''); } -// ─── Deploy Lock ────────────────────────────────────────────────────────────── +// ─── Validation helpers ─────────────────────────────────────────────────────── + +// PM2 process names: alphanumeric, hyphen, underscore, dot only +const SAFE_PROC_NAME_RE = /^[a-zA-Z0-9_.\-]+$/; +function isSafeProcName(name) { return SAFE_PROC_NAME_RE.test(name); } + +// Ensure filePath stays within ROOT to prevent path traversal +function isSafeFilePath(filePath) { + const resolved = path.resolve(filePath); + return resolved.startsWith(path.resolve(ROOT) + path.sep) || resolved === path.resolve(ROOT); +} + + function lockDeploy(reason = '') { const info = { lockedAt: new Date().toISOString(), reason, pid: process.pid }; @@ -99,7 +112,9 @@ function lockDeploy(reason = '') { } function unlockDeploy() { - try { fs.unlinkSync(LOCK_FILE); } catch { /* already removed */ } + try { fs.unlinkSync(LOCK_FILE); } catch (e) { + if (e.code !== 'ENOENT') log('UNLOCK_ERR', e.message); + } state.deployLocked = false; state.deployLockInfo = null; log('DEPLOY_UNLOCK', 'Deploy deblocat'); @@ -108,7 +123,13 @@ function unlockDeploy() { function isDeployLocked() { try { if (fs.existsSync(LOCK_FILE)) { - const info = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf8') || '{}'); + let info; + try { + info = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf8') || '{}'); + } catch (parseErr) { + log('LOCK_PARSE_ERR', parseErr.message); + info = {}; + } state.deployLocked = true; state.deployLockInfo = info; // Auto-expire lock după 30 minute @@ -120,7 +141,9 @@ function isDeployLocked() { } return true; } - } catch { /* ignore */ } + } catch (e) { + log('LOCK_CHECK_ERR', e.message); + } state.deployLocked = false; state.deployLockInfo = null; return false; @@ -193,9 +216,19 @@ function _handleMissingFile(filePath) { } function _restoreFileFromGit(filePath) { + if (!isSafeFilePath(filePath)) { + log('FILE_RESTORE_BLOCKED', `Path unsafe, skipping restore: ${filePath}`); + return; + } const relPath = path.relative(ROOT, filePath); + // Validate relPath contains no shell-special characters + if (/[;&|`$<>\\]/.test(relPath)) { + log('FILE_RESTORE_BLOCKED', `Path contains unsafe characters: ${relPath}`); + return; + } log('FILE_RESTORE', `Restaurare din git: ${relPath}`); - exec(`git -C "${ROOT}" checkout -- "${relPath}"`, (err, stdout, stderr) => { + const { execFile } = require('child_process'); + execFile('git', ['-C', ROOT, 'checkout', '--', relPath], (err) => { if (err) { log('FILE_RESTORE_ERR', relPath, err.message); _notify('file_restore_failed', { file: relPath, error: err.message }); @@ -241,6 +274,10 @@ function _checkProcesses() { } function _restartProcess(procName) { + if (!isSafeProcName(procName)) { + log('RESTART_BLOCKED', `Process name contains unsafe characters: ${procName}`); + return; + } const now = Date.now(); const last = lastHealAt[procName] || 0; if (now - last < HEAL_COOLDOWN_MS) { @@ -251,18 +288,19 @@ function _restartProcess(procName) { state.healCount++; log('RESTART_PROC', `pm2 restart ${procName}`); - exec(`pm2 restart "${procName}"`, { timeout: 15000 }, (err, stdout) => { + const { execFile } = require('child_process'); + execFile('pm2', ['restart', procName], { timeout: 15000 }, (err, stdout) => { if (err) { log('RESTART_ERR', procName, err.message); _notify('process_restart_failed', { process: procName, error: err.message }); - // Încercare start dacă restart a eşuat - exec(`pm2 startOrRestart ecosystem.config.js --only "${procName}"`, { cwd: ROOT, timeout: 20000 }, (err2) => { + // Încercare startOrRestart dacă restart a eşuat + execFile('pm2', ['startOrRestart', 'ecosystem.config.js', '--only', procName], { cwd: ROOT, timeout: 20000 }, (err2) => { if (err2) log('START_ERR', procName, err2.message); else { log('START_OK', procName); state.restarted.push({ process: procName, at: new Date().toISOString() }); } }); } else { log('RESTART_OK', procName); - state.restarted.push({ process: procName, at: new Date().toISOString(), out: stdout.slice(0, 100) }); + state.restarted.push({ process: procName, at: new Date().toISOString(), out: (stdout || '').slice(0, 100) }); _notify('process_restarted', { process: procName }); } }); @@ -284,7 +322,7 @@ function _notify(event, data) { headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }, timeout: 5000, }, (res) => { res.resume(); }); - req.on('error', () => { /* silently ignore notification failures */ }); + req.on('error', (e) => { log('NOTIFY_ERR', `${NOTIFY_URL}: ${e.message}`); }); req.write(payload); req.end(); } catch { /* ignore */ } From 70ce11f7f7d79f8ab6f3f9f0dba5290cfcd6b766 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:59:51 +0000 Subject: [PATCH 3/3] fix: remaining security issues - execFile for pm2 jlist, robust path check, fix tainted-format-string in health-guardian log Agent-Logs-Url: https://github.com/ruffy80/ZeusAI/sessions/d8f4af31-6da5-4108-8ec4-4b73cafe0e53 Co-authored-by: ruffy80 <29306714+ruffy80@users.noreply.github.com> --- UNICORN_FINAL/scripts/health-guardian.js | 4 ++-- UNICORN_FINAL/scripts/system-shield.js | 17 +++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/UNICORN_FINAL/scripts/health-guardian.js b/UNICORN_FINAL/scripts/health-guardian.js index b785d170..ecc3d0e8 100644 --- a/UNICORN_FINAL/scripts/health-guardian.js +++ b/UNICORN_FINAL/scripts/health-guardian.js @@ -24,9 +24,9 @@ let consecutiveExternalFailures = 0; function log(msg, extra) { const ts = new Date().toISOString(); if (extra) { - console.log(`[HealthGuardian] ${ts} ${msg}`, extra); + console.log('[HealthGuardian]', ts, String(msg), extra); } else { - console.log(`[HealthGuardian] ${ts} ${msg}`); + console.log('[HealthGuardian]', ts, String(msg)); } } diff --git a/UNICORN_FINAL/scripts/system-shield.js b/UNICORN_FINAL/scripts/system-shield.js index ec370899..955cd2ca 100644 --- a/UNICORN_FINAL/scripts/system-shield.js +++ b/UNICORN_FINAL/scripts/system-shield.js @@ -32,7 +32,8 @@ const path = require('path'); const fs = require('fs'); -const { exec, execSync } = require('child_process'); +const { execSync } = require('child_process'); +const { execFile } = require('child_process'); const http = require('http'); // ─── Config ─────────────────────────────────────────────────────────────────── @@ -97,8 +98,10 @@ function isSafeProcName(name) { return SAFE_PROC_NAME_RE.test(name); } // Ensure filePath stays within ROOT to prevent path traversal function isSafeFilePath(filePath) { - const resolved = path.resolve(filePath); - return resolved.startsWith(path.resolve(ROOT) + path.sep) || resolved === path.resolve(ROOT); + const resolvedRoot = path.resolve(ROOT); + const resolvedFile = path.resolve(filePath); + const rel = path.relative(resolvedRoot, resolvedFile); + return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel); } @@ -226,8 +229,7 @@ function _restoreFileFromGit(filePath) { log('FILE_RESTORE_BLOCKED', `Path contains unsafe characters: ${relPath}`); return; } - log('FILE_RESTORE', `Restaurare din git: ${relPath}`); - const { execFile } = require('child_process'); + log('FILE_RESTORE', 'Restaurare din git: ' + relPath); execFile('git', ['-C', ROOT, 'checkout', '--', relPath], (err) => { if (err) { log('FILE_RESTORE_ERR', relPath, err.message); @@ -249,7 +251,7 @@ function startProcessMonitor() { } function _checkProcesses() { - exec('pm2 jlist', { timeout: 10000 }, (err, stdout) => { + execFile('pm2', ['jlist'], { timeout: 10000 }, (err, stdout) => { if (err) { log('PM2_UNAVAILABLE', err.message); return; @@ -287,8 +289,7 @@ function _restartProcess(procName) { lastHealAt[procName] = now; state.healCount++; - log('RESTART_PROC', `pm2 restart ${procName}`); - const { execFile } = require('child_process'); + log('RESTART_PROC', 'pm2 restart ' + procName); execFile('pm2', ['restart', procName], { timeout: 15000 }, (err, stdout) => { if (err) { log('RESTART_ERR', procName, err.message);