文件预览

generate_attestation.mjs

查看 hermes-attestation-guardian 技能包中的文件内容。

文件内容

scripts/generate_attestation.mjs

#!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import {
  buildAttestation,
  defaultOutputPath,
  parseAttestationPolicy,
  resolveHermesScopedOutputPath,
  sha256FileHex,
  stableStringify,
} from "../lib/attestation.mjs";

function usage() {
  process.stdout.write(
    [
      "Usage: node scripts/generate_attestation.mjs [options]",
      "",
      "Options:",
      "  --output <path>          Output file path (default: ~/.hermes/security/attestations/current.json)",
      "  --policy <path>          JSON policy file with watch_files and trust_anchor_files arrays",
      "  --watch <path>           Extra watched file path (repeatable)",
      "  --trust-anchor <path>    Extra trust anchor file path (repeatable)",
      "  --generated-at <iso>     Override generated_at for deterministic testing",
      "  --write-sha256           Also write <output>.sha256 with file digest",
      "  --compact                Write compact JSON (no indentation)",
      "  --help                   Show this help",
      "",
    ].join("\n"),
  );
}

function parseArgs(argv) {
  const args = {
    output: defaultOutputPath(),
    policyPath: null,
    watch: [],
    trustAnchor: [],
    generatedAt: process.env.HERMES_ATTESTATION_GENERATED_AT || null,
    writeSha256: false,
    compact: false,
  };

  for (let i = 0; i < argv.length; i += 1) {
    const token = argv[i];

    if (token === "--help") {
      args.help = true;
      continue;
    }
    if (token === "--output") {
      args.output = argv[i + 1];
      i += 1;
      continue;
    }
    if (token === "--policy") {
      args.policyPath = argv[i + 1];
      i += 1;
      continue;
    }
    if (token === "--watch") {
      args.watch.push(argv[i + 1]);
      i += 1;
      continue;
    }
    if (token === "--trust-anchor") {
      args.trustAnchor.push(argv[i + 1]);
      i += 1;
      continue;
    }
    if (token === "--generated-at") {
      args.generatedAt = argv[i + 1];
      i += 1;
      continue;
    }
    if (token === "--write-sha256") {
      args.writeSha256 = true;
      continue;
    }
    if (token === "--compact") {
      args.compact = true;
      continue;
    }

    throw new Error(`Unknown argument: ${token}`);
  }

  return args;
}

function isSymlinkPath(filePath) {
  try {
    return fs.lstatSync(filePath).isSymbolicLink();
  } catch (error) {
    if (error?.code === "ENOENT") {
      return false;
    }
    throw error;
  }
}

function writeAtomically(outPath, body) {
  const dir = path.dirname(outPath);
  const base = path.basename(outPath);
  const tempPath = path.join(dir, `.${base}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
  let fd = null;

  try {
    fd = fs.openSync(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
    fs.writeFileSync(fd, body, "utf8");
    fs.fsyncSync(fd);
    fs.closeSync(fd);
    fd = null;

    if (isSymlinkPath(outPath)) {
      throw new Error(`output path must not be a symlink: ${outPath}`);
    }

    fs.renameSync(tempPath, outPath);
  } finally {
    if (fd !== null) {
      try {
        fs.closeSync(fd);
      } catch {
        // best-effort cleanup
      }
    }
    if (fs.existsSync(tempPath)) {
      fs.unlinkSync(tempPath);
    }
  }
}

function run() {
  const args = parseArgs(process.argv.slice(2));
  if (args.help) {
    usage();
    return;
  }

  if (args.generatedAt && Number.isNaN(Date.parse(args.generatedAt))) {
    throw new Error(`Invalid --generated-at value: ${args.generatedAt}`);
  }

  const policy = args.policyPath
    ? parseAttestationPolicy(fs.readFileSync(path.resolve(args.policyPath), "utf8"))
    : parseAttestationPolicy(null);

  const attestation = buildAttestation({
    generatedAt: args.generatedAt,
    policy,
    extraWatchFiles: args.watch,
    extraTrustAnchorFiles: args.trustAnchor,
  });

  const outPath = resolveHermesScopedOutputPath(args.output);
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
  const body = stableStringify(attestation, args.compact ? 0 : 2);
  writeAtomically(outPath, `${body}\n`);

  if (args.writeSha256) {
    const shaPath = `${outPath}.sha256`;
    const digest = sha256FileHex(outPath);
    fs.writeFileSync(shaPath, `${digest}  ${path.basename(outPath)}\n`, "utf8");
  }

  process.stdout.write(
    `${stableStringify({
      level: "INFO",
      message: "attestation generated",
      output: outPath,
      canonical_sha256: attestation.digests.canonical_sha256,
    })}\n`,
  );
}

try {
  run();
} catch (error) {
  process.stderr.write(`CRITICAL: ${error?.message || String(error)}\n`);
  process.exit(1);
}