← back to Dw Boardroom V2
src/engine/governanceClient.ts
131 lines
// ═══════════════════════════════════════════════
// DW Boardroom V2 — Governance API Client
// All persistent governance data flows through port 4020
// ═══════════════════════════════════════════════
import fetch from 'node-fetch';
import type { GovDecision, GovDelegation, GovEscalation, GovStatus, GovDecisionStats, DecisionType } from '../types';
const GOVERNANCE_URL = process.env.GOVERNANCE_URL || 'http://127.0.0.1:4020';
const AUTH_PASS = process.env.BOARDROOM_AUTH_PASS || process.env.AUTH_PASS;
if (!AUTH_PASS) throw new Error('BOARDROOM_AUTH_PASS not set — route via /secrets');
const AUTH = Buffer.from(`${process.env.AUTH_USER || 'admin'}:${AUTH_PASS}`).toString('base64');
const headers = {
'Content-Type': 'application/json',
'Authorization': `Basic ${AUTH}`,
};
async function govFetch<T>(path: string, options: any = {}): Promise<T> {
const url = `${GOVERNANCE_URL}${path}`;
try {
const res = await fetch(url, { headers, ...options });
if (!res.ok) {
const text = await res.text();
throw new Error(`Governance API ${res.status}: ${text}`);
}
return await res.json() as T;
} catch (err: any) {
if (err.code === 'ECONNREFUSED') {
console.error(`[govClient] Governance API unavailable at ${GOVERNANCE_URL}`);
throw new Error('Governance API unavailable');
}
throw err;
}
}
// ─── Decisions ─────────────────────────────────
export async function createDecision(
topic: string,
decisionType: DecisionType,
recommendation: string,
owner: string,
source: string = 'boardroom-v2'
): Promise<GovDecision> {
return govFetch<GovDecision>('/api/decisions', {
method: 'POST',
body: JSON.stringify({ topic, decisionType, recommendation, owner, source }),
});
}
export async function approveDecision(id: string): Promise<GovDecision> {
return govFetch<GovDecision>(`/api/decisions/${id}/approve`, { method: 'POST' });
}
export async function rejectDecision(id: string, reason?: string): Promise<GovDecision> {
return govFetch<GovDecision>(`/api/decisions/${id}/reject`, {
method: 'POST',
body: JSON.stringify({ reason }),
});
}
export async function deferDecision(id: string): Promise<GovDecision> {
return govFetch<GovDecision>(`/api/decisions/${id}/defer`, { method: 'POST' });
}
export async function getDecisions(status?: string): Promise<GovDecision[]> {
const qs = status ? `?status=${status}` : '';
return govFetch<GovDecision[]>(`/api/decisions${qs}`);
}
export async function getDecisionStats(): Promise<GovDecisionStats> {
return govFetch<GovDecisionStats>('/api/decisions/stats');
}
// ─── Delegations ───────────────────────────────
export async function createDelegation(
owner: string,
delegate: string,
autonomyLevel: number,
metric?: string
): Promise<GovDelegation> {
return govFetch<GovDelegation>('/api/delegations', {
method: 'POST',
body: JSON.stringify({ owner, delegate, autonomyLevel, metric }),
});
}
export async function suggestDelegate(query: string): Promise<any> {
return govFetch<any>('/api/delegations/suggest', {
method: 'POST',
body: JSON.stringify({ query }),
});
}
export async function getDelegations(): Promise<GovDelegation[]> {
return govFetch<GovDelegation[]>('/api/delegations');
}
// ─── Escalations ───────────────────────────────
export async function triggerEscalationCheck(): Promise<any> {
return govFetch<any>('/api/escalations/check', { method: 'POST' });
}
export async function getActiveEscalations(): Promise<GovEscalation[]> {
return govFetch<GovEscalation[]>('/api/escalations/active');
}
// ─── Governance Config ─────────────────────────
export async function getGovernanceStatus(): Promise<GovStatus> {
return govFetch<GovStatus>('/api/governance/status');
}
export async function getGovernanceConfig(): Promise<Record<string, any>> {
return govFetch<Record<string, any>>('/api/governance/config');
}
// ─── Health Check ──────────────────────────────
export async function isGovernanceAvailable(): Promise<boolean> {
try {
const res = await fetch(`${GOVERNANCE_URL}/health`);
return res.ok;
} catch {
return false;
}
}