Ctrl K
ComunidadGuías…

scripts/live-test-resource-ledger.mjs

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
/**
 * Exact-ID resource ledger for disposable Chickpea acceptance runs.
 *
 * The file is append-only JSONL. Cleanup tools must consume cleanupPlan() and
 * must record an exact provider verification after every deletion/archive.
 * Display names are evidence only and can never identify a cleanup target.
 */
import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

export const LEDGER_SCHEMA = 'chickpea-live-resource-ledger/v1';
const RESOURCE_KINDS = new Set([
  'worker', 'd1', 'slack_app', 'slack_channel', 'capture_worker', 'capture_store',
  'build_connection', 'generated_repository', 'container_app', 'container_image',
]);
const PROVIDERS = new Set(['cloudflare', 'slack', 'github']);
const ID_PATTERNS = new Map([
  ['cloudflare:worker', /^[a-z0-9][a-z0-9_-]{0,62}$/],
  ['cloudflare:capture_worker', /^[a-z0-9][a-z0-9_-]{0,62}$/],
  ['cloudflare:d1', /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i],
  ['cloudflare:capture_store', /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i],
  ['cloudflare:container_app', /^[A-Za-z0-9][A-Za-z0-9._:-]{2,511}$/],
  ['cloudflare:container_image', /^[A-Za-z0-9][A-Za-z0-9._/@:-]{2,511}$/],
  ['slack:slack_app', /^A[A-Z0-9]+$/],
  ['slack:slack_channel', /^C[A-Z0-9]+$/],
  ['github:generated_repository', /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/],
]);
const FORBIDDEN_KEY = /(secret|token|password|credential|authorization|cookie|raw(?:Body|Prompt)?)/i;
const CLEANUP_PRIORITY = new Map([
  ['cloudflare:worker', 10],
  ['cloudflare:capture_worker', 10],
  ['cloudflare:container_app', 20],
  ['cloudflare:container_image', 30],
  ['cloudflare:d1', 40],
  ['cloudflare:capture_store', 40],
  ['slack:slack_app', 50],
  ['slack:slack_channel', 60],
  ['cloudflare:build_connection', 70],
  ['github:generated_repository', 80],
]);

export function createLedger(pathInput, { runId = `agent-first-${randomUUID()}`, now = Date.now() } = {}) {
  const path = resolve(pathInput);
  if (existsSync(path)) throw new Error(`Ledger already exists: ${path}`);
  writeFileSync(path, `${JSON.stringify({ schema: LEDGER_SCHEMA, type: 'run', runId, at: now })}\n`, {
    encoding: 'utf8',
    flag: 'wx',
    mode: 0o600,
  });
  return { path, runId };
}

export function appendLedgerEvent(pathInput, event) {
  const path = resolve(pathInput);
  const ledger = readLedger(path);
  const normalized = validateEvent({ ...event, runId: ledger.runId });
  appendFileSync(path, `${JSON.stringify(normalized)}\n`, { encoding: 'utf8', mode: 0o600 });
  return normalized;
}

export function readLedger(pathInput) {
  const path = resolve(pathInput);
  if (!existsSync(path)) throw new Error(`Ledger not found: ${path}`);
  const lines = readFileSync(path, 'utf8').split('\n').filter(Boolean);
  if (lines.length === 0) throw new Error('Ledger is empty.');
  const events = lines.map((line, index) => {
    let parsed;
    try { parsed = JSON.parse(line); } catch { throw new Error(`Invalid ledger JSON at line ${index + 1}.`); }
    rejectSecrets(parsed);
    return parsed;
  });
  const head = events[0];
  if (head?.schema !== LEDGER_SCHEMA || head.type !== 'run' || !isExactId(head.runId)) {
    throw new Error('Ledger header is invalid.');
  }
  if (events.some((event) => event.runId !== head.runId)) throw new Error('Ledger contains a foreign run event.');
  return { path, runId: head.runId, events };
}

export function cleanupPlan(pathInput) {
  const ledger = readLedger(pathInput);
  const resources = new Map();
  const verified = new Set();
  for (const event of ledger.events) {
    if (event.type === 'resource') resources.set(resourceKey(event), event);
    if (event.type === 'cleanup_verified') verified.add(resourceKey(event));
  }
  const targets = [];
  for (const resource of resources.values()) {
    if (verified.has(resourceKey(resource))) continue;
    assertCleanupEligible(resource);
    targets.push({
      provider: resource.provider,
      kind: resource.kind,
      id: resource.id,
      action: resource.kind === 'slack_channel' ? 'archive' : 'delete',
      ...(resource.name ? { name: resource.name } : {}),
    });
  }
  return targets.sort((a, b) =>
    (CLEANUP_PRIORITY.get(`${a.provider}:${a.kind}`) ?? 999) -
      (CLEANUP_PRIORITY.get(`${b.provider}:${b.kind}`) ?? 999) ||
    resourceKey(a).localeCompare(resourceKey(b)));
}

export function compareInventory(pathInput, finalInventory) {
  const ledger = readLedger(pathInput);
  const authorizedBaselineCleanup = new Set(
    ledger.events
      .filter((event) => event.type === 'resource' && event.ownedByRun === false &&
        event.baselineAuthorizedChickpeaCleanup === true)
      .map(resourceKey),
  );
  const baselines = ledger.events.filter((event) =>
    event.type === 'baseline' && !authorizedBaselineCleanup.has(resourceKey(event)));
  const actual = normalizeInventory(finalInventory);
  const expected = normalizeInventory(baselines.map(({ provider, kind, id }) => ({ provider, kind, id })));
  const missing = expected.filter((item) => !actual.some((candidate) => resourceKey(candidate) === resourceKey(item)));
  const unexpected = actual.filter((item) => !expected.some((candidate) => resourceKey(candidate) === resourceKey(item)));
  return { equal: missing.length === 0 && unexpected.length === 0, missing, unexpected };
}

export function recordResource(path, input) {
  return appendLedgerEvent(path, {
    type: 'resource', at: input.at ?? Date.now(), provider: input.provider, kind: input.kind,
    id: input.id, ownedByRun: input.ownedByRun === true,
    baselineAuthorizedChickpeaCleanup: input.baselineAuthorizedChickpeaCleanup === true,
    ...(input.name ? { name: input.name } : {}),
  });
}

export function recordBaseline(path, input) {
  return appendLedgerEvent(path, {
    type: 'baseline', at: input.at ?? Date.now(), provider: input.provider, kind: input.kind,
    id: input.id, ...(input.name ? { name: input.name } : {}),
  });
}

export function recordCleanupVerified(path, input) {
  const ledger = readLedger(path);
  const target = ledger.events.find((event) => event.type === 'resource' &&
    event.provider === input.provider && event.kind === input.kind && event.id === input.id);
  if (!target) throw new Error('Cleanup verification does not match a recorded resource.');
  return appendLedgerEvent(path, {
    type: 'cleanup_verified', at: input.at ?? Date.now(), provider: input.provider,
    kind: input.kind, id: input.id, outcome: 'absent',
  });
}

function validateEvent(event) {
  rejectSecrets(event);
  if (!isExactId(event.runId)) throw new Error('Event requires an exact run ID.');
  if (!['baseline', 'resource', 'cleanup_verified', 'note'].includes(event.type)) {
    throw new Error(`Unsupported ledger event: ${String(event.type)}`);
  }
  if (event.type === 'note') return event;
  validateCoordinate(event);
  if (event.type === 'resource' && typeof event.ownedByRun !== 'boolean') {
    throw new Error('Resource ownership must be explicit.');
  }
  if (event.type === 'cleanup_verified' && event.outcome !== 'absent') {
    throw new Error('Cleanup is verified only by exact absence.');
  }
  return event;
}

function validateCoordinate(value) {
  if (!PROVIDERS.has(value.provider)) throw new Error(`Unsupported provider: ${String(value.provider)}`);
  if (!RESOURCE_KINDS.has(value.kind)) throw new Error(`Unsupported resource kind: ${String(value.kind)}`);
  if (!isExactId(value.id)) throw new Error('Cleanup targets require an immutable exact provider ID.');
  const expected = ID_PATTERNS.get(`${value.provider}:${value.kind}`);
  if (expected && !expected.test(value.id)) {
    throw new Error('Cleanup targets require an immutable exact provider ID.');
  }
}

function assertCleanupEligible(resource) {
  validateCoordinate(resource);
  if (resource.ownedByRun === true) return;
  if (resource.provider === 'slack' && resource.kind === 'slack_app' &&
      resource.baselineAuthorizedChickpeaCleanup === true) return;
  throw new Error(`Refusing to clean baseline/unowned resource ${resourceKey(resource)}.`);
}

function normalizeInventory(items) {
  if (!Array.isArray(items)) throw new Error('Inventory must be an array.');
  return items.map((item) => {
    validateCoordinate(item);
    return { provider: item.provider, kind: item.kind, id: item.id };
  }).sort((a, b) => resourceKey(a).localeCompare(resourceKey(b)));
}

function resourceKey(value) {
  return `${value.provider}:${value.kind}:${value.id}`;
}

export function isExactId(value) {
  return typeof value === 'string' && value.length >= 3 && value.length <= 512 &&
    !/[\s*?\[\]{}]/u.test(value) && !value.startsWith('-') && value !== '.' && value !== '..';
}

function rejectSecrets(value, path = '') {
  if (!value || typeof value !== 'object') return;
  for (const [key, child] of Object.entries(value)) {
    const childPath = path ? `${path}.${key}` : key;
    if (FORBIDDEN_KEY.test(key)) throw new Error(`Secret-bearing ledger field is forbidden: ${childPath}`);
    if (typeof child === 'string' && /^(?:xox[baprs]-|sk-|Bearer\s)/i.test(child)) {
      throw new Error(`Secret-like ledger value is forbidden: ${childPath}`);
    }
    rejectSecrets(child, childPath);
  }
}

function value(args, flag) {
  const index = args.indexOf(flag);
  return index >= 0 ? args[index + 1] : undefined;
}

async function cli() {
  const args = process.argv.slice(2);
  const command = args[0];
  const path = value(args, '--ledger');
  if (!path) throw new Error('Pass --ledger <absolute-or-relative-path>.');
  if (command === 'create') {
    console.log(JSON.stringify(createLedger(path, { ...(value(args, '--run-id') ? { runId: value(args, '--run-id') } : {}) })));
    return;
  }
  if (command === 'plan') {
    console.log(JSON.stringify(cleanupPlan(path), null, 2));
    return;
  }
  throw new Error('Usage: live-test-resource-ledger.mjs <create|plan> --ledger <path> [--run-id <id>]');
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  cli().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; });
}