Ctrl K
ComunidadGuías…

privacy-status.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * privacy-status.mjs — diagnostic for the outbound LLM privacy guard.
 *
 * WHY THIS EXISTS
 * ---------------
 * A guard that is silently misconfigured is worse than no guard, because it
 * looks like protection. This command answers, without sending anything:
 *
 *   - is the guard running, and which config file did it read?
 *   - how many configured values are actually being guarded, and how many were
 *     dropped for being too short to guard safely?
 *   - which pattern detectors are active?
 *   - is the process-level bypass in effect?
 *
 * It never prints a configured value. It reports counts and categories only, in
 * the same discipline the guard's error messages follow — because a diagnostic
 * that echoes the secret into a terminal, a screenshot or a chat log defeats the
 * thing it is diagnosing.
 *
 * USAGE
 *   node privacy-status.mjs
 *   node privacy-status.mjs --json     # machine-readable, for scripts and CI
 *
 * EXIT CODES
 *   0  guard active and usable (including the documented "no config" state)
 *   1  the guard could not be loaded (config present but unreadable/malformed)
 *   2  the guard is explicitly bypassed for this process
 */

import { existsSync } from 'node:fs';
// Load .env before reading privacy-guard config so PRIVACY_GUARD and
// PRIVACY_BYPASS_TOKEN from .env are applied consistently with other
// evaluator entrypoints.
import 'dotenv/config';
import {
  AUTO_DETECT_DEFAULTS,
  PrivacyConfigError,
  bypassRequested,
  getPrivacyConfigPath,
  getPrivacyExamplePath,
  loadPrivacyGuard,
  readPrivacyConfig,
} from './lib/privacy-guard.mjs';
import { LLM_HOSTS } from './lib/llm-outbound.mjs';

const asJson = process.argv.includes('--json');

/**
 * @typedef {object} StatusReport
 * @property {'active'|'bypassed'|'disabled-by-config'|'config-error'} state
 * @property {string} configPath
 * @property {boolean} configPresent
 * @property {string} configSource
 * @property {number} configuredCount
 * @property {number} [rawConfiguredCount]
 * @property {Record<string, boolean>} autoDetect
 * @property {number} llmHostCount
 * @property {string} [error]
 */

/** @returns {StatusReport} */
function collect() {
  const configPath = getPrivacyConfigPath();
  const report = {
    state: 'active',
    configPath,
    configPresent: existsSync(configPath),
    configSource: 'unknown',
    configuredCount: 0,
    rawConfiguredCount: 0,
    autoDetect: { ...AUTO_DETECT_DEFAULTS },
    llmHostCount: LLM_HOSTS.length,
  };

  // Check the two-factor process bypass before parsing configuration so
  // PRIVACY_GUARD=off + PRIVACY_BYPASS_TOKEN is honored even if config is malformed.
  if (bypassRequested()) {
    report.state = 'bypassed';
    return report;
  }

  try {
    const config = readPrivacyConfig();
    report.configSource = config.source;
    report.autoDetect = { ...AUTO_DETECT_DEFAULTS, ...config.autoDetect };

    const guard = loadPrivacyGuard({ config });
    if (!guard.enabled) {
      // Guard disabled by configuration (enabled: false in privacy.yml).
      report.state = 'disabled-by-config';
      return report;
    }
    report.configuredCount = guard.configuredCount;
    report.rawConfiguredCount = config.values?.length ?? 0;
    return report;
  } catch (err) {
    if (err instanceof PrivacyConfigError) {
      report.state = 'config-error';
      const reason = err.message ? err.message.replace(/^privacy:\s*/i, '').trim() : 'unreadable configuration';
      report.error = reason.includes(configPath)
        ? reason
        : `Could not load configuration file at ${configPath}: ${reason}`;
      return report;
    }
    throw err;
  }
}

function render(report) {
  const lines = [];
  lines.push('career-ops privacy guard — status');
  lines.push('');
  lines.push(`  state            ${report.state}`);
  lines.push(`  config file      ${report.configPath}`);
  lines.push(`  config present   ${report.configPresent ? 'yes' : 'no'}`);
  lines.push(`  config source    ${report.configSource}`);
  lines.push(`  guarded values   ${report.configuredCount}`);

  const active = Object.entries(report.autoDetect)
    .filter(([, on]) => on)
    .map(([k]) => k);
  const off = Object.entries(report.autoDetect)
    .filter(([, on]) => !on)
    .map(([k]) => k);
  lines.push(`  auto-detect on   ${active.length ? active.join(', ') : '(none)'}`);
  lines.push(`  auto-detect off  ${off.length ? off.join(', ') : '(none)'}`);
  lines.push(`  LLM hosts known  ${report.llmHostCount}`);
  lines.push('');

  if (report.state === 'config-error') {
    lines.push('  ⚠️  The guard could NOT be loaded:');
    lines.push(`      ${report.error}`);
    lines.push('      The guard refuses to run on a config it cannot read. Fix the file,');
    lines.push('      or remove it to disable all configured checks.');
    return lines.join('\n');
  }

  if (report.state === 'bypassed') {
    lines.push('  ⚠️  The guard is BYPASSED for this process');
    lines.push('      (PRIVACY_GUARD=off with PRIVACY_BYPASS_TOKEN set).');
    lines.push('      Nothing is being inspected. Unset both environment variables to re-enable.');
    return lines.join('\n');
  }

  if (report.state === 'disabled-by-config') {
    lines.push('  ℹ️  The guard is disabled by configuration (enabled: false in privacy.yml).');
    lines.push(`      Nothing is being inspected. Set enabled: true in ${report.configPath} to re-enable.`);
    return lines.join('\n');
  }

  if (report.configSource === 'absent') {
    lines.push(`  ℹ️  No ${report.configPath} — privacy guard is inactive (opt-in).`);
    lines.push('      To guard your own values and enable pattern detection:');
    lines.push(`        copy ${getPrivacyExamplePath()}`);
    lines.push(`          →  ${report.configPath}`);
    lines.push('      and replace the fictional placeholders with your own.');
  } else if (report.configSource === 'empty') {
    lines.push(`  ℹ️  ${report.configPath} exists but configures no values.`);
  } else if (report.configuredCount === 0) {
    if (report.rawConfiguredCount > 0) {
      lines.push(`  ℹ️  ${report.configPath} loaded, but every value was too short to guard`);
      lines.push('      safely (5+ characters, 5+ digits). Nothing configured is protected.');
    } else if (active.length > 0) {
      lines.push(`  ✅ Guard active: running with pattern detection only; the enabled`);
      lines.push('     patterns above are blocked on outbound LLM requests.');
    } else {
      lines.push(`  ℹ️  ${report.configPath} exists but configures no values.`);
    }
  } else {
    lines.push(`  ✅ Guard active: ${report.configuredCount} configured value(s) and`);
    lines.push('     the enabled patterns above are blocked on outbound LLM requests.');
  }
  return lines.join('\n');
}

const report = collect();
if (asJson) {
  console.log(JSON.stringify(report, null, 2));
} else {
  console.log(render(report));
}

if (report.state === 'config-error') process.exit(1);
if (report.state === 'bypassed') process.exit(2);
process.exit(0);