← Files ForgeMindARCHIVED FILE

src/forge/trust.mjs

9.4 KB · Oct 2, 2026 · 00:31 UTC

↓ Download file

import { ForgeMindError } from '../errors.mjs';
import { appendFlightEvent } from './flight.mjs';
import { stableId, verifyRecord } from './integrity.mjs';
import { saveForgeRecord } from './store.mjs';

const SUPPORTED_VENDORS = ['openai', 'anthropic', 'github', 'cursor', 'ci', 'custom'];

export async function createTrustContract({ workspace, input, now = new Date() }) {
  validateContract(input);
  const normalized = {
    schemaVersion: 1,
    protocol: 'forgemind-agent-trust-v1',
    id: stableId('contract', input),
    createdAt: now.toISOString(),
    title: input.title,
    intent: input.intent,
    acceptanceCriteria: input.acceptanceCriteria.map((criterion) => ({
      id: criterion.id,
      description: criterion.description,
      evidenceType: criterion.evidenceType,
      mandatory: criterion.mandatory !== false,
    })),
    requiredEvidence: [...new Set(input.requiredEvidence)].sort(),
    policy: {
      allowedVendors: [...new Set(input.policy?.allowedVendors ?? SUPPORTED_VENDORS)].sort(),
      requireNoViolations: input.policy?.requireNoViolations !== false,
      requiredApprovals: [...new Set(input.policy?.requiredApprovals ?? [])].sort(),
    },
    rollbackRequired: Boolean(input.rollbackRequired),
    budgets: {
      maxDurationMinutes: finiteOrNull(input.budgets?.maxDurationMinutes),
      maxCostUnits: finiteOrNull(input.budgets?.maxCostUnits),
    },
  };
  const saved = await saveForgeRecord({ workspace, area: 'trust/contracts', record: normalized });
  await appendFlightEvent({ workspace, event: { capability: 'trust', action: 'create', subject: saved.record.id, status: 'created' }, now });
  return { schemaVersion: 1, status: 'created', record: saved.record, evidencePath: saved.path, errors: [] };
}

export async function importAgentEvidence({ workspace, input, now = new Date() }) {
  validateEvidence(input);
  const normalized = {
    schemaVersion: 1,
    protocol: 'forgemind-agent-evidence-v1',
    id: stableId('agent_evidence', { producer: input.producer, repository: input.repository, acceptance: input.acceptance, artifacts: input.artifacts }),
    importedAt: now.toISOString(),
    untrustedContent: true,
    producer: {
      vendor: input.producer.vendor.toLowerCase(),
      agent: input.producer.agent,
      model: input.producer.model ?? null,
      runId: input.producer.runId,
    },
    repository: { commit: input.repository?.commit ?? null, dirty: Boolean(input.repository?.dirty) },
    acceptance: input.acceptance.map((item) => ({ criterionId: item.criterionId, status: item.status, evidence: [...new Set(item.evidence ?? [])].sort() })),
    artifacts: input.artifacts.map((item) => ({ type: item.type, path: item.path, status: item.status, digest: item.digest ?? null })),
    verification: { status: input.verification?.status ?? 'missing', commands: [...new Set(input.verification?.commands ?? [])] },
    policy: { violations: [...new Set(input.policy?.violations ?? [])], approvals: [...new Set(input.policy?.approvals ?? [])] },
    rollback: { status: input.rollback?.status ?? 'missing', steps: [...new Set(input.rollback?.steps ?? [])] },
    actuals: { durationMinutes: finiteOrNull(input.actuals?.durationMinutes), costUnits: finiteOrNull(input.actuals?.costUnits) },
    notes: input.notes ?? null,
  };
  const saved = await saveForgeRecord({ workspace, area: 'trust/evidence', record: normalized });
  await appendFlightEvent({ workspace, event: { capability: 'trust', action: 'import', subject: saved.record.id, status: 'imported', producer: normalized.producer.vendor }, now });
  return { schemaVersion: 1, status: 'imported', record: saved.record, evidencePath: saved.path, errors: [] };
}

export async function evaluateTrust({ workspace, contract, evidence, now = new Date() }) {
  requireValidRecord(contract, 'contract');
  requireValidRecord(evidence, 'evidence');
  const acceptanceMissing = contract.acceptanceCriteria.filter((criterion) => criterion.mandatory).filter((criterion) => {
    const result = evidence.acceptance.find((item) => item.criterionId === criterion.id);
    const artifact = evidence.artifacts.find((item) => item.type === criterion.evidenceType && item.status === 'passed');
    return result?.status !== 'passed' || !artifact;
  }).map((criterion) => criterion.id);
  const requiredEvidenceMissing = contract.requiredEvidence.filter((type) => !evidence.artifacts.some((artifact) => artifact.type === type && artifact.status === 'passed'));
  const acceptancePassed = acceptanceMissing.length === 0 && requiredEvidenceMissing.length === 0;
  const verificationPassed = evidence.verification.status === 'passed' && evidence.verification.commands.length > 0;
  const vendorAllowed = contract.policy.allowedVendors.includes(evidence.producer.vendor);
  const policyPassed = vendorAllowed && (!contract.policy.requireNoViolations || evidence.policy.violations.length === 0)
    && contract.policy.requiredApprovals.every((approval) => evidence.policy.approvals.includes(approval));
  const provenancePassed = Boolean(evidence.producer.vendor && evidence.producer.agent && evidence.producer.runId && evidence.repository.commit);
  const rollbackPassed = !contract.rollbackRequired || (evidence.rollback.status === 'documented' && evidence.rollback.steps.length > 0);
  const budgetFailures = [];
  if (contract.budgets.maxDurationMinutes !== null && (evidence.actuals.durationMinutes === null || evidence.actuals.durationMinutes > contract.budgets.maxDurationMinutes)) budgetFailures.push('duration');
  if (contract.budgets.maxCostUnits !== null && (evidence.actuals.costUnits === null || evidence.actuals.costUnits > contract.budgets.maxCostUnits)) budgetFailures.push('cost');
  const gates = [
    gate('acceptance-and-required-evidence', acceptancePassed, 35, [...acceptanceMissing, ...requiredEvidenceMissing]),
    gate('verification', verificationPassed, 25, verificationPassed ? [] : ['verification']),
    gate('policy', policyPassed, 20, [...(vendorAllowed ? [] : ['vendor']), ...evidence.policy.violations, ...contract.policy.requiredApprovals.filter((approval) => !evidence.policy.approvals.includes(approval))]),
    gate('provenance', provenancePassed, 10, provenancePassed ? [] : ['producer-or-commit']),
    gate('rollback', rollbackPassed, 10, rollbackPassed ? [] : ['rollback']),
    gate('budget', budgetFailures.length === 0, 0, budgetFailures),
  ];
  const blockers = gates.filter((item) => item.status === 'failed').flatMap((item) => item.details.map((detail) => `${item.name}:${detail}`));
  const score = gates.filter((item) => item.status === 'passed').reduce((sum, item) => sum + item.weight, 0);
  const status = blockers.length ? 'rejected' : 'trusted';
  const record = {
    schemaVersion: 1,
    protocol: 'forgemind-trust-attestation-v1',
    id: stableId('attestation', { contract: contract.id, evidence: evidence.id, score, blockers }),
    evaluatedAt: now.toISOString(),
    contractId: contract.id,
    contractDigest: contract.digest.value,
    evidenceId: evidence.id,
    evidenceDigest: evidence.digest.value,
    status,
    score,
    gates,
    blockers,
  };
  const saved = await saveForgeRecord({ workspace, area: 'trust/attestations', record });
  await appendFlightEvent({ workspace, event: { capability: 'trust', action: 'evaluate', subject: saved.record.id, status, contractId: contract.id, evidenceId: evidence.id }, now });
  return { ...saved.record, errors: [] };
}

export function verifyTrustRecord(record) {
  return verifyRecord(record);
}

function gate(name, passed, weight, details) { return { name, status: passed ? 'passed' : 'failed', weight, details }; }

function requireValidRecord(record, name) {
  if (verifyRecord(record).status !== 'valid') throw new ForgeMindError('FM_TRUST_RECORD_INVALID', `Trust ${name} record is missing or tampered.`);
}

function validateContract(input) {
  for (const field of ['title', 'intent']) if (!String(input?.[field] ?? '').trim()) throw new ForgeMindError('FM_TRUST_CONTRACT_INVALID', `Trust contract field is required: ${field}`);
  if (!Array.isArray(input.acceptanceCriteria) || input.acceptanceCriteria.length === 0) throw new ForgeMindError('FM_TRUST_CONTRACT_INVALID', 'Trust contract requires acceptance criteria.');
  if (!Array.isArray(input.requiredEvidence) || input.requiredEvidence.length === 0) throw new ForgeMindError('FM_TRUST_CONTRACT_INVALID', 'Trust contract requires evidence types.');
  for (const criterion of input.acceptanceCriteria) for (const field of ['id', 'description', 'evidenceType']) if (!String(criterion[field] ?? '').trim()) throw new ForgeMindError('FM_TRUST_CONTRACT_INVALID', `Acceptance criterion field is required: ${field}`);
}

function validateEvidence(input) {
  for (const field of ['vendor', 'agent', 'runId']) if (!String(input?.producer?.[field] ?? '').trim()) throw new ForgeMindError('FM_TRUST_EVIDENCE_INVALID', `Evidence producer field is required: ${field}`);
  if (!SUPPORTED_VENDORS.includes(input.producer.vendor.toLowerCase())) throw new ForgeMindError('FM_TRUST_VENDOR_UNSUPPORTED', `Unsupported evidence vendor: ${input.producer.vendor}`);
  if (!Array.isArray(input.acceptance) || !Array.isArray(input.artifacts)) throw new ForgeMindError('FM_TRUST_EVIDENCE_INVALID', 'Evidence acceptance and artifacts must be arrays.');
}

function finiteOrNull(value) { return value === undefined || value === null || value === '' ? null : Number.isFinite(Number(value)) ? Number(value) : null; }

SHA-256: ffa93c284827994a5704a2a02a2c129ff4ca30d5ebce8775ea8d0d568ce250e4