← 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;
  }
}