242 lines
8.7 KiB
JavaScript
242 lines
8.7 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
|
// EOS Engineering Contract — Hårda regler som agenter måste följa
|
||
|
|
// Erik-krav: "Det ska inte vara rekommendationer utan hårda regler"
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
import { execSync } from 'child_process';
|
||
|
|
import { existsSync, appendFileSync } from 'fs';
|
||
|
|
|
||
|
|
const VIOLATION_LOG = '/home/bernt/.openclaw/workspace/EOS/contract-violations.jsonl';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Hårda regler — ingen agent får bryta dessa
|
||
|
|
*/
|
||
|
|
// ── Princip: Verifierbar användning ───────────────────────────────────────
|
||
|
|
// "Ingen komponent anses existera förrän den producerar verifierbara
|
||
|
|
// artefakter från verklig användning."
|
||
|
|
// ──────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
const CONTRACT_RULES = [
|
||
|
|
{
|
||
|
|
id: 'no-ssh-prod',
|
||
|
|
rule: 'Ingen direkt SSH till produktion',
|
||
|
|
check: () => {
|
||
|
|
// Kontrollera att ingen SSH-nyckel är aktiv mot produktion
|
||
|
|
try {
|
||
|
|
const sshConfig = execSync('cat ~/.ssh/config 2>/dev/null || echo "NO_CONFIG"', { encoding: 'utf8' });
|
||
|
|
const hasProdSSH = sshConfig.match(/Host.*prod|Host.*production/i);
|
||
|
|
return {
|
||
|
|
valid: !hasProdSSH,
|
||
|
|
evidence: hasProdSSH ? 'SSH-config innehåller produktionshost' : 'Ingen produktions-SSH hittad'
|
||
|
|
};
|
||
|
|
} catch {
|
||
|
|
return { valid: true, evidence: 'Ingen SSH-config hittad' };
|
||
|
|
}
|
||
|
|
},
|
||
|
|
consequence: 'STOP'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'no-server-edit',
|
||
|
|
rule: 'Ingen redigering på server',
|
||
|
|
check: () => {
|
||
|
|
// Kontrollera att vi inte är på en server
|
||
|
|
const isServer = existsSync('/etc/ec2-release') || existsSync('/var/lib/cloud');
|
||
|
|
return {
|
||
|
|
valid: !isServer,
|
||
|
|
evidence: isServer ? 'Kör på server (EC2/cloud-init hittat)' : 'Kör lokalt'
|
||
|
|
};
|
||
|
|
},
|
||
|
|
consequence: 'STOP'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'no-uncommitted-changes',
|
||
|
|
rule: 'Ingen ändring utan commit',
|
||
|
|
check: () => {
|
||
|
|
try {
|
||
|
|
const status = execSync('git status --short', {
|
||
|
|
cwd: '/home/bernt/repos/quixzoom.com',
|
||
|
|
encoding: 'utf8'
|
||
|
|
});
|
||
|
|
return {
|
||
|
|
valid: status.trim().length === 0,
|
||
|
|
evidence: status.trim().length > 0 ? `Ocommitade ändringar: ${status.trim().split("\n").length} filer` : 'Allt commitat'
|
||
|
|
};
|
||
|
|
} catch {
|
||
|
|
return { valid: false, evidence: 'Kunde inte köra git status' };
|
||
|
|
}
|
||
|
|
},
|
||
|
|
consequence: 'ESCALATE'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'no-deploy-without-pipeline',
|
||
|
|
rule: 'Ingen deployment utan pipeline',
|
||
|
|
check: () => {
|
||
|
|
const hasCI = existsSync('/home/bernt/repos/quixzoom.com/.github/workflows') ||
|
||
|
|
existsSync('/home/bernt/repos/quixzoom.com/.gitlab-ci.yml');
|
||
|
|
return {
|
||
|
|
valid: hasCI,
|
||
|
|
evidence: hasCI ? 'CI/CD pipeline hittad' : 'Ingen CI/CD pipeline hittad'
|
||
|
|
};
|
||
|
|
},
|
||
|
|
consequence: 'STOP'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'no-migration-without-version',
|
||
|
|
rule: 'Ingen migration utan versionshantering',
|
||
|
|
check: () => {
|
||
|
|
const hasMigrations = existsSync('/home/bernt/repos/quixzoom.com/migrations');
|
||
|
|
return {
|
||
|
|
valid: hasMigrations,
|
||
|
|
evidence: hasMigrations ? 'Migrationer versionshanterade' : 'Inga migrationer hittade'
|
||
|
|
};
|
||
|
|
},
|
||
|
|
consequence: 'STOP'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'no-rollback-without-verification',
|
||
|
|
rule: 'Ingen rollback utan verifierad återställningspunkt',
|
||
|
|
check: () => {
|
||
|
|
// Kontrollera att det finns backup/rollback-mekanism
|
||
|
|
const hasBackup = existsSync('/home/bernt/repos/quixzoom.com/scripts/backup.sh') ||
|
||
|
|
existsSync('/home/bernt/repos/quixzoom.com/scripts/rollback.sh');
|
||
|
|
return {
|
||
|
|
valid: hasBackup,
|
||
|
|
evidence: hasBackup ? 'Backup/rollback scripts hittade' : 'Inga backup/rollback scripts'
|
||
|
|
};
|
||
|
|
},
|
||
|
|
consequence: 'ESCALATE'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'no-code-without-tests',
|
||
|
|
rule: 'Ingen kod utan tester',
|
||
|
|
check: () => {
|
||
|
|
const hasTests = existsSync('/home/bernt/repos/quixzoom.com/tests') ||
|
||
|
|
existsSync('/home/bernt/repos/quixzoom.com/__tests__');
|
||
|
|
return {
|
||
|
|
valid: hasTests,
|
||
|
|
evidence: hasTests ? 'Tester hittade' : 'Inga tester hittade'
|
||
|
|
};
|
||
|
|
},
|
||
|
|
consequence: 'ESCALATE'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'no-merge-without-analysis',
|
||
|
|
rule: 'Ingen merge utan analys',
|
||
|
|
check: () => {
|
||
|
|
const hasSIL = existsSync('/home/bernt/.openclaw/workspace/SIL/pr-analyzer.mjs');
|
||
|
|
return {
|
||
|
|
valid: hasSIL,
|
||
|
|
evidence: hasSIL ? 'SIL finns för PR-analys' : 'SIL saknas'
|
||
|
|
};
|
||
|
|
},
|
||
|
|
consequence: 'ESCALATE'
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
class EngineeringContract {
|
||
|
|
constructor() {
|
||
|
|
this.rules = CONTRACT_RULES;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validera alla kontraktsregler
|
||
|
|
*/
|
||
|
|
validate() {
|
||
|
|
console.log('🔍 Validerar Engineering Contract...\n');
|
||
|
|
|
||
|
|
const results = [];
|
||
|
|
let stopViolations = 0;
|
||
|
|
let escalateViolations = 0;
|
||
|
|
|
||
|
|
for (const rule of this.rules) {
|
||
|
|
const result = rule.check();
|
||
|
|
|
||
|
|
results.push({
|
||
|
|
id: rule.id,
|
||
|
|
rule: rule.rule,
|
||
|
|
valid: result.valid,
|
||
|
|
evidence: result.evidence,
|
||
|
|
consequence: rule.consequence
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!result.valid) {
|
||
|
|
if (rule.consequence === 'STOP') stopViolations++;
|
||
|
|
if (rule.consequence === 'ESCALATE') escalateViolations++;
|
||
|
|
|
||
|
|
this.logViolation({
|
||
|
|
rule: rule.id,
|
||
|
|
description: rule.rule,
|
||
|
|
consequence: rule.consequence,
|
||
|
|
evidence: result.evidence
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const icon = result.valid ? '✅' : rule.consequence === 'STOP' ? '🔴' : '🟡';
|
||
|
|
console.log(`${icon} ${rule.rule}`);
|
||
|
|
console.log(` ${result.evidence}`);
|
||
|
|
console.log(` Konsekvens: ${rule.consequence}`);
|
||
|
|
console.log();
|
||
|
|
}
|
||
|
|
|
||
|
|
const report = {
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
total: this.rules.length,
|
||
|
|
valid: this.rules.length - stopViolations - escalateViolations,
|
||
|
|
stopViolations,
|
||
|
|
escalateViolations,
|
||
|
|
results
|
||
|
|
};
|
||
|
|
|
||
|
|
this.printReport(report);
|
||
|
|
return report;
|
||
|
|
}
|
||
|
|
|
||
|
|
logViolation(violation) {
|
||
|
|
appendFileSync(VIOLATION_LOG, JSON.stringify({
|
||
|
|
...violation,
|
||
|
|
timestamp: new Date().toISOString()
|
||
|
|
}) + '\n', 'utf8');
|
||
|
|
}
|
||
|
|
|
||
|
|
printReport(report) {
|
||
|
|
console.log('╔═══════════════════════════════════════════════════════════════╗');
|
||
|
|
console.log('║ ENGINEERING CONTRACT ║');
|
||
|
|
console.log('║ Hårda regler för agenter ║');
|
||
|
|
console.log('╚═══════════════════════════════════════════════════════════════╝');
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
console.log(`📊 RESULTAT\n`);
|
||
|
|
console.log(` Regler: ${report.total}`);
|
||
|
|
console.log(` Godkända: ${report.valid} ✅`);
|
||
|
|
console.log(` STOP-violations: ${report.stopViolations} 🔴`);
|
||
|
|
console.log(` ESCALATE-violations: ${report.escalateViolations} 🟡`);
|
||
|
|
console.log();
|
||
|
|
|
||
|
|
if (report.stopViolations > 0) {
|
||
|
|
console.log('🔴 STOP — Agenter får INTE fortsätta\n');
|
||
|
|
for (const result of report.results.filter(r => !r.valid && r.consequence === 'STOP')) {
|
||
|
|
console.log(` • ${result.rule}`);
|
||
|
|
console.log(` ${result.evidence}`);
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (report.escalateViolations > 0) {
|
||
|
|
console.log('🟡 ESCALATE — Kräver mänsklig granskning\n');
|
||
|
|
for (const result of report.results.filter(r => !r.valid && r.consequence === 'ESCALATE')) {
|
||
|
|
console.log(` • ${result.rule}`);
|
||
|
|
console.log(` ${result.evidence}`);
|
||
|
|
}
|
||
|
|
console.log();
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('═══════════════════════════════════════════════════════════════\n');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Main ──────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
const contract = new EngineeringContract();
|
||
|
|
contract.validate();
|