agents/drift-report.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
// drift-report.mjs — turn a `probe-installed --allow-newer --json` result into one
// GitHub issue per agent whose @latest binary is DRIFTED (newer than the pinned
// tested_agent_version) or BROKEN (failed to launch, or not newer than the pin).
// Idempotent: one open issue per agent id, updated in place rather than duplicated.
// Non-blocking by design — the caller runs it with continue-on-error, and this script
// never exits non-zero on a `gh` hiccup, only on bad input.
//
// Usage: node agents/drift-report.mjs --input <probe.json>
// Requires the `gh` CLI authenticated (GH_TOKEN) with `issues: write`.
import { readFileSync, statSync } from "node:fs";
import { cmpVersion } from "./version.mjs";
import { spawnSync } from "node:child_process";
import { basename, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const PROBE_SCHEMA = "caveman.agent-probe.v1";
const MAX_PROBE_BYTES = 1024 * 1024;
const MAX_RESULTS = 64;
const VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
function invalid(message) {
process.stderr.write(`drift-report: invalid probe artifact: ${message}\n`);
process.exit(2);
}
function exactKeys(value, keys) {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
}
function boundedString(value, max, { empty = false, controls = false } = {}) {
return typeof value === "string"
&& (empty || value.length > 0)
&& value.length <= max
&& (controls || !/[\u0000-\u001f\u007f]/.test(value));
}
let registry;
try {
registry = JSON.parse(readFileSync(join(here, "agents.json"), "utf8"));
} catch (error) {
invalid(`trusted registry cannot be read: ${error.message}`);
}
if (registry?.schema_version !== "1" || !Array.isArray(registry.agents)) {
invalid("trusted registry must use schema_version 1 with an agents array");
}
const profiles = new Map();
for (const profile of registry.agents) {
if (!profile || typeof profile !== "object" || typeof profile.id !== "string" || profiles.has(profile.id)) {
invalid("trusted registry contains an invalid or duplicate profile id");
}
if (!boundedString(profile.tested_agent_version, 128)
|| (profile.tested_agent_version !== "x" && !VERSION_RE.test(profile.tested_agent_version))) {
invalid(`trusted registry profile ${profile.id} has an invalid tested_agent_version`);
}
profiles.set(profile.id, profile);
}
if (profiles.size === 0 || profiles.size > MAX_RESULTS) invalid(`trusted registry profile count must be between 1 and ${MAX_RESULTS}`);
const args = process.argv.slice(2);
if (args.length !== 4 || args[0] !== "--input" || !args[1] || args[2] !== "--expected-id" || !args[3]) {
process.stderr.write("usage: node agents/drift-report.mjs --input <probe.json> --expected-id <profile-id>\n");
process.exit(2);
}
const expectedId = args[3];
if (!profiles.has(expectedId)) invalid(`--expected-id must name a known profile (got ${JSON.stringify(expectedId)})`);
if (basename(args[1]) !== `${expectedId}.json`) {
invalid(`input basename must exactly match expected profile id (${expectedId}.json)`);
}
let parsed;
try {
const stat = statSync(args[1]);
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_PROBE_BYTES) {
invalid(`input must be a non-empty regular file no larger than ${MAX_PROBE_BYTES} bytes`);
}
parsed = JSON.parse(readFileSync(args[1], "utf8"));
} catch (error) {
invalid(`cannot read probe json: ${error.message}`);
}
if (!exactKeys(parsed, ["schema", "results"]) || parsed.schema !== PROBE_SCHEMA || !Array.isArray(parsed.results)) {
invalid(`top level must contain exactly schema=${JSON.stringify(PROBE_SCHEMA)} and results[]`);
}
if (parsed.results.length !== profiles.size || parsed.results.length > MAX_RESULTS) {
invalid(`results must contain the complete ${profiles.size}-profile registry set`);
}
const seen = new Set();
const statusKeys = {
"missing": ["id", "status", "tested"],
"not-installed": ["id", "status", "tested"],
"ok": ["id", "status", "binary", "tested", "observed", "version_ok", "help_ok", "version_matches"],
"drift": ["id", "status", "binary", "tested", "observed", "version_ok", "help_ok", "version_matches"],
"broken": ["id", "status", "binary", "tested", "observed", "version_ok", "help_ok", "version_matches", "version_error", "help_error"],
};
for (const [index, result] of parsed.results.entries()) {
const label = `results[${index}]`;
if (!result || typeof result !== "object" || Array.isArray(result)) invalid(`${label} must be an object`);
if (!boundedString(result.id, 64) || !profiles.has(result.id)) invalid(`${label}.id must name a known profile`);
if (seen.has(result.id)) invalid(`${label}.id duplicates profile ${result.id}`);
seen.add(result.id);
const expectedKeys = statusKeys[result.status];
if (!expectedKeys || !exactKeys(result, expectedKeys)) invalid(`${label} has unknown status or fields outside its exact status schema`);
const profile = profiles.get(result.id);
if (result.tested !== profile.tested_agent_version) invalid(`${label}.tested must exactly equal registry tested_agent_version for ${result.id}`);
if (result.status === "missing" || result.status === "not-installed") continue;
if (!boundedString(result.binary, 4096)) invalid(`${label}.binary must be a bounded control-free string`);
if (typeof result.version_ok !== "boolean" || typeof result.help_ok !== "boolean" || typeof result.version_matches !== "boolean") {
invalid(`${label} probe flags must be booleans`);
}
if (!boundedString(result.observed, 128, { empty: true }) || (result.observed !== "" && !VERSION_RE.test(result.observed))) {
invalid(`${label}.observed must be empty or a strict version`);
}
const expectedMatch = result.tested === "x" || result.observed === result.tested;
if (result.version_matches !== expectedMatch) invalid(`${label}.version_matches contradicts tested and observed versions`);
if (result.status === "ok") {
if (!result.version_ok || !result.help_ok || !result.version_matches || !VERSION_RE.test(result.observed)) {
invalid(`${label} ok status requires launchable probes and an exact version match`);
}
} else if (result.status === "drift") {
if (!result.version_ok || !result.help_ok || result.version_matches || result.tested === "x"
|| !VERSION_RE.test(result.observed) || cmpVersion(result.observed, result.tested) <= 0) {
invalid(`${label} drift status requires a strict observed version newer than tested`);
}
if (result.id !== expectedId) invalid(`${label} cannot report drift for ${result.id}; artifact is bound to ${expectedId}`);
} else {
if (result.version_ok && result.help_ok && result.version_matches) invalid(`${label} broken status contradicts successful matching probes`);
if (!boundedString(result.version_error, 131072, { empty: true, controls: true })
|| !boundedString(result.help_error, 131072, { empty: true, controls: true })) {
invalid(`${label} broken probe errors must be bounded strings`);
}
}
}
for (const id of profiles.keys()) {
if (!seen.has(id)) invalid(`results omit expected registry profile ${id}`);
}
const expectedResult = parsed.results.find((result) => result.id === expectedId);
if (!expectedResult || expectedResult.status === "not-installed") {
invalid(`expected profile ${expectedId} must carry its required-probe result`);
}
// No issue-write-capable subprocess runs before every byte of the downloaded artifact
// has passed the closed validation above.
const drifted = expectedResult.status === "drift" || expectedResult.status === "broken" ? [expectedResult] : [];
if (drifted.length === 0) {
process.stdout.write("drift-report: no drift observed\n");
process.exit(0);
}
const runUrl = process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID
? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
: "";
function gh(argv) {
const result = spawnSync("gh", argv, { encoding: "utf8" });
return { ok: result.status === 0 && !result.error, stdout: result.stdout || "", stderr: result.stderr || result.error?.message || "" };
}
for (const r of drifted) {
const broken = r.status === "broken";
const observed = r.observed || "unknown";
const title = broken
? `agent-drift: ${r.id} — installed ${observed} fails the latest probe (pinned ${r.tested})`
: `agent-drift: ${r.id} — installed ${r.observed} exceeds pinned ${r.tested}`;
const marker = `<!-- caveman-agent-drift:${r.id} -->`;
// The state line doubles as the "already current" key below, so drift and broken
// at the same version must not be the same line.
const stateLine = `- installed (@latest): \`${observed}\`${broken ? " (probe broken)" : ""}`;
// A broken probe has exactly one of two causes and the artifact already says which:
// a surface that did not launch, or a clean launch that yielded no newer version.
// Reporting them as a disjunction ("it did not launch ... or ...") contradicted the
// flags printed beside it and made the maintainer re-run the probe to find out
// which it was (#1236).
const unlaunchable = broken
? [...(r.version_ok ? [] : ["`--version`"]), ...(r.help_ok ? [] : ["`--help`"])]
: [];
const launched = [...(r.version_ok ? ["`--version`"] : []), ...(r.help_ok ? ["`--help`"] : [])];
let brokenSentence = null;
if (!broken) {
// Not computed for a drift result: every branch below describes a probe
// failure, and a drift result is a clean probe.
} else if (unlaunchable.length === 0) {
brokenSentence = `The \`${r.id}\` @latest binary launched on both \`--version\` and \`--help\`, but the version it reported (\`${observed}\`) is not newer than the pin, so the probe cannot confirm a drift.`;
} else if (launched.length === 0) {
brokenSentence = `The \`${r.id}\` @latest binary installed, but neither \`--version\` nor \`--help\` launched under the isolated probe environment.`;
} else {
brokenSentence = `The \`${r.id}\` @latest binary installed and ${launched.join(" and ")} launched, but ${unlaunchable.join(" and ")} failed under the isolated probe environment.`;
}
// version_error/help_error stay out of the issue: they are untrusted upstream output.
const body = [
marker,
"",
broken
? brokenSentence
: `The \`${r.id}\` upstream released a version newer than the pin the profile claims to test.`,
"",
stateLine,
`- profile \`tested_agent_version\`: \`${r.tested}\``,
...(broken && runUrl ? [`- probe run (artifact \`agent-probe-${r.id}\`): ${runUrl}`] : []),
"",
"This is a non-blocking drift report from the nightly Agent conformance workflow. To clear it:",
broken
? (unlaunchable.length
? `1. Find why ${unlaunchable.join(" and ")} fails for \`${r.id}@latest\`; the captured output is in the probe artifact, not copied here. If the latest-drift install line tracks something other than a release, point it at one.`
: `1. Find why the probe could not read a newer version for \`${r.id}@latest\`; if the latest-drift install line tracks something other than a release, point it at one.`)
: `1. Verify \`${r.id}@${r.observed}\` wraps correctly.`,
`2. Bump \`tested_agent_version\` in \`agents/profiles/${r.id}.json\` (the conformance CI pin is derived from it and enforced by \`compile.mjs\`).`,
`3. Update \`last_verified_at\`/\`verified_by\` if the profile carries them.`,
].join("\n");
// One open issue per agent id. GitHub's full-text search tokenizer mangles the
// punctuation-heavy marker, so a search-based lookup misses and re-opens duplicates every
// run. Instead LIST open issues and filter locally by the stable title prefix or the body
// marker — deterministic, no tokenizer in the loop.
const titlePrefix = `agent-drift: ${r.id} `;
const list = gh(["issue", "list", "--state", "open", "--limit", "200", "--json", "number,title,body"]);
let existing;
if (list.ok) {
try {
existing = JSON.parse(list.stdout).find((issue) =>
(typeof issue.title === "string" && issue.title.startsWith(titlePrefix)) ||
(typeof issue.body === "string" && issue.body.includes(marker)));
} catch {
existing = undefined;
}
} else {
process.stdout.write(`drift-report: could not list issues for ${r.id}: ${list.stderr}\n`);
continue;
}
if (existing) {
// GitHub returns CRLF bodies once an issue is edited in the web UI.
if ((existing.body || "").split(/\r?\n/).includes(stateLine)) {
process.stdout.write(`drift-report: ${r.id} issue #${existing.number} already current\n`);
continue;
}
const edit = gh(["issue", "edit", String(existing.number), "--body", body, "--title", title]);
process.stdout.write(edit.ok ? `drift-report: updated ${r.id} issue #${existing.number}\n` : `drift-report: failed to update ${r.id}: ${edit.stderr}\n`);
} else {
const created = gh(["issue", "create", "--title", title, "--body", body]);
process.stdout.write(created.ok ? `drift-report: opened ${r.id} issue\n` : `drift-report: failed to open ${r.id} issue: ${created.stderr}\n`);
}
}
process.exit(0);