05ed037fe8
- DNS: pilot.landvex.com -> 16.170.83.169 - TLS: Let's Encrypt certificate (expires 2026-09-30) - Nginx: reverse proxy with SSL termination - API: https://pilot.landvex.com/api/v1/missions - UI: https://pilot.landvex.com/ - Upload: POST /api/v1/missions/import (multipart/form-data) Verified: ✅ https://pilot.landvex.com/health ✅ https://pilot.landvex.com/version ✅ https://pilot.landvex.com/api/v1/missions (list) ✅ https://pilot.landvex.com/api/v1/missions/:id (get) ✅ POST /api/v1/missions/import (video upload) ✅ UI loads with title 'LandveX Intelligence Lab' Next: Pilot 001 — Break the system!
253 lines
9.6 KiB
JavaScript
253 lines
9.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// EOS Policy Enforcement v2 — Förklara VARFÖR och HUR man häver blockering
|
|
// Erik-krav: "Ett bra enforcement-system säger inte bara NEJ. Det säger: vilken regel, varför, evidens, och exakt vad som krävs."
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
import { execSync } from 'child_process';
|
|
import { existsSync, appendFileSync } from 'fs';
|
|
|
|
const ENFORCEMENT_LOG = '/home/bernt/.openclaw/workspace/EOS/enforcement-log-v2.jsonl';
|
|
|
|
/**
|
|
* Policyer med förklaringar och vägar tillbaka
|
|
*/
|
|
const POLICIES = [
|
|
{
|
|
id: 'block-direct-prod',
|
|
name: 'Blockera direkt skrivning till produktion',
|
|
rule: 'Ingen agent får skriva direkt till produktion',
|
|
why: 'Direkta ändringar i produktion kan inte spåras, testas eller återställas. De skapar "drift" som skiljer sig från kodbasen.',
|
|
evidence: (action) => `Kör på server: ${existsSync('/etc/ec2-release') ? 'Ja' : 'Nej'}`,
|
|
requiredToUnblock: [
|
|
'Gör ändringen i en branch',
|
|
'Skapa PR för code review',
|
|
'Låt CI köra tester',
|
|
'Mergea till main',
|
|
'Låt pipeline deploya'
|
|
],
|
|
check: (action) => {
|
|
const isProd = process.env.NODE_ENV === 'production' || existsSync('/etc/ec2-release');
|
|
const isWrite = action.type === 'write' || action.type === 'deploy';
|
|
return {
|
|
blocked: isProd && isWrite,
|
|
reason: isProd && isWrite ? 'Direkt skrivning till produktion' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
id: 'block-no-pipeline-deploy',
|
|
name: 'Blockera deployment utan pipeline',
|
|
rule: 'Ingen deployment utan godkänd CI/CD pipeline',
|
|
why: 'Manuell deployment är felbenägen och kan inte reproduceras. Pipeline säkerställer att tester körs och att deployment är spårbart.',
|
|
evidence: (action) => `CI/CD hittad: ${existsSync('/home/bernt/repos/quixzoom.com/.github/workflows') ? 'Ja' : 'Nej'}`,
|
|
requiredToUnblock: [
|
|
'Skapa .github/workflows/deploy.yml',
|
|
'Konfigurera deployment-steg',
|
|
'Verifiera att pipeline kan köras',
|
|
'Dokumentera pipeline i README'
|
|
],
|
|
check: (action) => {
|
|
if (action.type !== 'deploy') return { blocked: false };
|
|
const hasPipeline = existsSync('/home/bernt/repos/quixzoom.com/.github/workflows') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/.gitlab-ci.yml');
|
|
return {
|
|
blocked: !hasPipeline,
|
|
reason: !hasPipeline ? 'Ingen CI/CD pipeline konfigurerad' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
id: 'block-server-edit',
|
|
name: 'Blockera redigering på server',
|
|
rule: 'Ingen redigering direkt på server',
|
|
why: 'Ändringar på server är inte versionshanterade och kan förloras vid omstart eller deployment. All kod ska gå via Git.',
|
|
evidence: (action) => `Kör på server: ${existsSync('/etc/ec2-release') ? 'Ja' : 'Nej'}`,
|
|
requiredToUnblock: [
|
|
'Gör ändringen lokalt eller i en dev-miljö',
|
|
'Commita ändringen',
|
|
'Pusha till remote',
|
|
'Låt CI/CD deploya till server'
|
|
],
|
|
check: (action) => {
|
|
const isServer = existsSync('/etc/ec2-release') || existsSync('/var/lib/cloud');
|
|
const isEdit = action.type === 'edit' || action.type === 'write';
|
|
return {
|
|
blocked: isServer && isEdit,
|
|
reason: isServer && isEdit ? 'Redigering på server' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
id: 'block-uncommitted',
|
|
name: 'Blockera ändring utan commit',
|
|
rule: 'Ingen ny ändring när ocommitade filer finns',
|
|
why: 'Ocommitade ändringar riskerar att gå förlorade eller konflikta med nya ändringar. Varje ändring ska vara spårbart.',
|
|
evidence: (action) => {
|
|
try {
|
|
const status = execSync('git status --short', {
|
|
cwd: '/home/bernt/repos/quixzoom.com',
|
|
encoding: 'utf8'
|
|
});
|
|
const count = status.trim().split('\n').filter(l => l.trim()).length;
|
|
return `${count} ocommitade filer`;
|
|
} catch {
|
|
return 'Kunde inte kontrollera git-status';
|
|
}
|
|
},
|
|
requiredToUnblock: [
|
|
'Kör "git status" för att se ocommitade filer',
|
|
'Bestäm: commita eller återställ varje fil',
|
|
'Kör "git add" för filer som ska commitas',
|
|
'Kör "git commit -m \"beskrivande meddelande\""',
|
|
'Kör "git push" för att synka med remote'
|
|
],
|
|
check: (action) => {
|
|
if (action.type === 'commit') return { blocked: false };
|
|
try {
|
|
const status = execSync('git status --short', {
|
|
cwd: '/home/bernt/repos/quixzoom.com',
|
|
encoding: 'utf8'
|
|
});
|
|
const hasUncommitted = status.trim().length > 0;
|
|
return {
|
|
blocked: hasUncommitted && (action.type === 'write' || action.type === 'edit'),
|
|
reason: hasUncommitted ? 'Ocommitade ändringar finns' : null
|
|
};
|
|
} catch {
|
|
return { blocked: true, reason: 'Kunde inte verifiera git-status' };
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'block-no-migration',
|
|
name: 'Blockera databasändring utan migration',
|
|
rule: 'Ingen databasändring utan versionshanterad migration',
|
|
why: 'Databasändringar utan migration kan inte reproduceras eller återställas. De riskerar att bryta andra miljöer.',
|
|
evidence: (action) => `Migrationer hittade: ${existsSync('/home/bernt/repos/quixzoom.com/migrations') ? 'Ja' : 'Nej'}`,
|
|
requiredToUnblock: [
|
|
'Skapa migrations-katalog om den saknas',
|
|
'Använd Prisma/TypeORM/Sequelize för att generera migration',
|
|
'Verifiera migration lokalt',
|
|
'Commita migration-filen',
|
|
'Dokumentera migrationsprocessen'
|
|
],
|
|
check: (action) => {
|
|
if (!action.target?.match(/database|db|sql/i)) return { blocked: false };
|
|
const hasMigration = existsSync('/home/bernt/repos/quixzoom.com/migrations') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/prisma');
|
|
return {
|
|
blocked: !hasMigration,
|
|
reason: !hasMigration ? 'Inga databasmigrationer konfigurerade' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
id: 'block-no-api-contract',
|
|
name: 'Blockera API-ändring utan kontrakt',
|
|
rule: 'Ingen API-ändring utan uppdaterat kontrakt',
|
|
why: 'API-ändringar utan kontrakt bryter konsumenter. OpenAPI är den officiella sanningen om API:ets gränssnitt.',
|
|
evidence: (action) => `OpenAPI hittad: ${existsSync('/home/bernt/repos/quixzoom.com/openapi.yaml') ? 'Ja' : 'Nej'}`,
|
|
requiredToUnblock: [
|
|
'Skapa openapi.yaml om den saknas',
|
|
'Uppdatera OpenAPI med nya endpoints/ändringar',
|
|
'Verifiera att kontraktet är validerbart',
|
|
'Commita ändringar i både kod och kontrakt',
|
|
'Uppdatera API-dokumentation'
|
|
],
|
|
check: (action) => {
|
|
if (!action.target?.match(/api|endpoint|route/i)) return { blocked: false };
|
|
const hasContract = existsSync('/home/bernt/repos/quixzoom.com/openapi.yaml') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/openapi.json');
|
|
return {
|
|
blocked: !hasContract,
|
|
reason: !hasContract ? 'Inget API-kontrakt (OpenAPI) hittat' : null
|
|
};
|
|
}
|
|
}
|
|
];
|
|
|
|
class PolicyEnforcementV2 {
|
|
constructor() {
|
|
this.policies = POLICIES;
|
|
}
|
|
|
|
validateAction(action) {
|
|
console.log(`🔍 Validerar åtgärd: ${action.type} ${action.target || ''}\n`);
|
|
|
|
const violations = [];
|
|
|
|
for (const policy of this.policies) {
|
|
const result = policy.check(action);
|
|
|
|
if (result.blocked) {
|
|
const violation = {
|
|
policy: policy.id,
|
|
name: policy.name,
|
|
rule: policy.rule,
|
|
why: policy.why,
|
|
evidence: typeof policy.evidence === 'function' ? policy.evidence(action) : policy.evidence,
|
|
requiredToUnblock: policy.requiredToUnblock,
|
|
reason: result.reason
|
|
};
|
|
|
|
violations.push(violation);
|
|
|
|
this.logEnforcement({
|
|
action,
|
|
policy: policy.id,
|
|
blocked: true,
|
|
reason: result.reason,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
action,
|
|
allowed: violations.length === 0,
|
|
violations,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
this.printResult(result);
|
|
return result;
|
|
}
|
|
|
|
printResult(result) {
|
|
if (result.allowed) {
|
|
console.log('✅ ÅTGÄRD GODKÄND\n');
|
|
console.log(` ${result.action.type} ${result.action.target || ''} kan utföras`);
|
|
} else {
|
|
console.log('🔴 ÅTGÄRD BLOCKERAD\n');
|
|
console.log(` ${result.action.type} ${result.action.target || ''} kan INTE utföras\n`);
|
|
|
|
for (const v of result.violations) {
|
|
console.log(` 📋 REGEL: ${v.rule}`);
|
|
console.log(` ❓ VARFÖR: ${v.why}`);
|
|
console.log(` 📊 EVIDENS: ${v.evidence}`);
|
|
console.log();
|
|
console.log(' 🔓 FÖR ATT HÄVA BLOCKERINGEN:\n');
|
|
for (const step of v.requiredToUnblock) {
|
|
console.log(` 1. ${step}`);
|
|
}
|
|
console.log();
|
|
}
|
|
}
|
|
}
|
|
|
|
logEnforcement(entry) {
|
|
appendFileSync(ENFORCEMENT_LOG, JSON.stringify(entry) + '\n', 'utf8');
|
|
}
|
|
}
|
|
|
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
|
|
const enforcement = new PolicyEnforcementV2();
|
|
const action = {
|
|
type: process.argv[2] || 'deploy',
|
|
target: process.argv[3] || 'production'
|
|
};
|
|
|
|
enforcement.validateAction(action);
|