Files
playbook/antigravity-awesome-skills/tools/local-skill-reviewer/safe-bundle.js
T
2026-07-18 00:02:59 +00:00

204 lines
10 KiB
JavaScript

"use strict";
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
const {
ALLOWED_BUNDLE_ROOTS,
MAX_BUNDLE_BYTES,
MAX_BUNDLE_FILE_BYTES,
MAX_BUNDLE_FILES,
MAX_SKILL_BYTES,
} = require("./constants");
const { safeTempRoot } = require("./safe-io");
const { secretIdentifierLike } = require("./secret");
function posix(value) { return value.split(path.sep).join("/"); }
function assertSafeRelative(value, label = "path") {
if (typeof value !== "string" || !value || value.includes("\0") || /[\x00-\x1f\x7f]/.test(value)) {
throw new Error(`${label} contains invalid characters`);
}
if (path.isAbsolute(value) || value.includes("\\")) throw new Error(`${label} must be a POSIX relative path`);
if (secretIdentifierLike(value)) throw new Error(`${label} contains secret-like material`);
const normalized = path.posix.normalize(value);
if (normalized !== value || normalized === ".." || normalized.startsWith("../")) {
throw new Error(`${label} escapes its root`);
}
return value;
}
function sha256(buffer) { return crypto.createHash("sha256").update(buffer).digest("hex"); }
const REGULAR_GIT_MODES = new Set(["100644", "100755"]);
const TRACKED_CACHE = new Map();
function parseIndexRecords(output) {
const entries = new Map();
for (const record of output.toString("utf8").split("\0").filter(Boolean)) {
const full = record.match(/^(\d{6}) ([0-9a-f]{40,64}) (\d)\t([\s\S]+)$/);
if (!full) throw new Error("Unable to parse git index object");
assertSafeRelative(full[4], "Git index path");
if (full[3] !== "0") throw new Error(`Unmerged Git index entry rejected: ${full[4]}`);
if (entries.has(full[4])) throw new Error(`Duplicate Git index entry rejected: ${full[4]}`);
entries.set(full[4], full[1]);
if (!entries.objects) Object.defineProperty(entries, "objects", { value: new Map(), enumerable: false });
entries.objects.set(full[4], full[2]);
}
return entries;
}
function trackedFiles(repoRoot) {
const output = execFileSync("git", ["ls-files", "--stage", "-z", "--", "skills"], {
cwd: repoRoot,
encoding: null,
maxBuffer: 32 * 1024 * 1024,
});
const cacheId = `${fs.realpathSync(repoRoot)}\0${sha256(output)}`;
if (TRACKED_CACHE.has(cacheId)) return TRACKED_CACHE.get(cacheId);
const entries = parseIndexRecords(output);
const eligible = [...entries].filter(([filePath, mode]) => {
if (!REGULAR_GIT_MODES.has(mode) || !filePath.startsWith("skills/")) return false;
return filePath.endsWith("/SKILL.md") || /\/(?:references|scripts|assets)\//.test(filePath);
});
const oids = [...new Set(eligible.map(([filePath]) => entries.objects.get(filePath)))];
const input = `${oids.join("\n")}\n`;
const checked = execFileSync("git", ["cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], { cwd: repoRoot, input, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
let total = 0;
for (const line of checked.trim().split("\n").filter(Boolean)) {
const match = line.match(/^([0-9a-f]{40,64}) blob (\d+)$/);
if (!match) throw new Error("Unexpected frozen git object metadata");
total += Number(match[2]) + line.length + 2;
}
if (!Number.isSafeInteger(total) || total > 512 * 1024 * 1024) throw new Error("Frozen Git object corpus exceeds memory boundary");
const batch = execFileSync("git", ["cat-file", "--batch"], { cwd: repoRoot, input, encoding: null, maxBuffer: total + 1024 });
const blobs = new Map();
let offset = 0;
while (offset < batch.length) {
const newline = batch.indexOf(10, offset);
if (newline < 0) throw new Error("Truncated frozen Git object header");
const header = batch.subarray(offset, newline).toString("utf8");
const match = header.match(/^([0-9a-f]{40,64}) blob (\d+)$/);
if (!match) throw new Error("Unexpected frozen Git object header");
const size = Number(match[2]);
const start = newline + 1;
const end = start + size;
if (end >= batch.length || batch[end] !== 10) throw new Error("Truncated frozen Git object body");
blobs.set(match[1], Buffer.from(batch.subarray(start, end)));
offset = end + 1;
}
Object.defineProperty(entries, "blobs", { value: blobs, enumerable: false });
TRACKED_CACHE.set(cacheId, entries);
return entries;
}
function readTrackedFile(repoRoot, relPath, tracked, maxBytes, { requireUtf8 = true } = {}) {
assertSafeRelative(relPath);
const oid = tracked.objects?.get(relPath);
if (!oid || !/^[0-9a-f]{40,64}$/.test(oid)) throw new Error(`Missing frozen git object: ${relPath}`);
const bytes = tracked.blobs?.get(oid);
if (!bytes) throw new Error(`Frozen git object bytes unavailable: ${relPath}`);
if (bytes.length > maxBytes) throw new Error(`File exceeds byte limit: ${relPath}`);
const algorithm = oid.length === 40 ? "sha1" : "sha256";
const gitOid = crypto.createHash(algorithm).update(Buffer.from(`blob ${bytes.length}\0`)).update(bytes).digest("hex");
if (gitOid !== oid) throw new Error(`Frozen git object changed while reading: ${relPath}`);
let text = null;
try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch (error) { if (requireUtf8) throw error; }
return { path: relPath, bytes, text, encoding: text === null ? "binary" : "utf-8", sha256: sha256(bytes), size: bytes.length };
}
function readRegularFile(repoRoot, relPath, maxBytes, { requireUtf8 = true } = {}) {
assertSafeRelative(relPath);
const absolute = path.join(repoRoot, ...relPath.split("/"));
const rootReal = fs.realpathSync(repoRoot);
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
const fd = fs.openSync(absolute, flags);
let bytes;
let stat;
try {
stat = fs.fstatSync(fd);
if (!stat.isFile()) throw new Error(`Not a regular file: ${relPath}`);
if (stat.size > maxBytes) throw new Error(`File exceeds byte limit: ${relPath}`);
const real = fs.realpathSync(absolute);
if (real !== rootReal && !real.startsWith(`${rootReal}${path.sep}`)) throw new Error(`File escapes repository: ${relPath}`);
bytes = fs.readFileSync(fd);
const after = fs.fstatSync(fd);
if (after.dev !== stat.dev || after.ino !== stat.ino || after.size !== bytes.length || after.mtimeMs !== stat.mtimeMs) throw new Error(`File changed while reading: ${relPath}`);
} finally { fs.closeSync(fd); }
let text = null;
try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch (error) {
if (requireUtf8) throw error;
}
return { path: relPath, bytes, text, encoding: text === null ? "binary" : "utf-8", sha256: sha256(bytes), size: bytes.length };
}
function discoverBundle(repoRoot, skillId, tracked = trackedFiles(repoRoot)) {
assertSafeRelative(skillId, "skill id");
const base = `skills/${skillId}`;
const skillPath = `${base}/SKILL.md`;
if (!tracked.has(skillPath)) throw new Error(`Canonical skill is not tracked: ${skillPath}`);
if (!REGULAR_GIT_MODES.has(tracked.get(skillPath))) throw new Error(`Canonical skill has unsafe git mode: ${skillPath}`);
const primary = readTrackedFile(repoRoot, skillPath, tracked, MAX_SKILL_BYTES);
const candidates = [...tracked.keys()].filter((item) => item.startsWith(`${base}/`)).sort();
const files = [primary];
let bundleBytes = 0;
for (const relPath of candidates) {
if (relPath === skillPath) continue;
const suffix = relPath.slice(base.length + 1);
const root = suffix.split("/")[0];
if (!ALLOWED_BUNDLE_ROOTS.includes(root)) continue;
if (!REGULAR_GIT_MODES.has(tracked.get(relPath))) throw new Error(`Bundle file has unsafe git mode: ${relPath}`);
// Supplemental files participate byte-for-byte in bundle identity. Binary
// archives/assets are hashed but never parsed, rendered, or sent to Codex.
const file = readTrackedFile(repoRoot, relPath, tracked, MAX_BUNDLE_FILE_BYTES, { requireUtf8: false });
bundleBytes += file.size;
if (bundleBytes > MAX_BUNDLE_BYTES) throw new Error(`Bundle exceeds byte limit: ${skillId}`);
files.push(file);
if (files.length - 1 > MAX_BUNDLE_FILES) throw new Error(`Bundle exceeds file limit: ${skillId}`);
}
const identity = files.map((file) => `${file.path}\0${file.sha256}\0${file.size}`).join("\n");
return { skillId, skillPath, files, bundleHash: sha256(Buffer.from(identity)) };
}
function bundleMap(bundle) { return new Map(bundle.files.map((file) => [file.path, file.text])); }
function listCanonicalSkillIds(repoRoot, tracked = trackedFiles(repoRoot)) {
const result = [];
for (const [filePath, mode] of tracked.entries()) {
if (!filePath.startsWith("skills/") || !filePath.endsWith("/SKILL.md")) continue;
if (!REGULAR_GIT_MODES.has(mode)) throw new Error(`Canonical skill has unsafe git mode: ${filePath}`);
const skillId = filePath.slice("skills/".length, -"/SKILL.md".length);
result.push(skillId);
}
return result.sort();
}
function materializeBundle(bundle) {
const root = fs.mkdtempSync(path.join(safeTempRoot(), "aas-review-snapshot-"));
try {
for (const file of bundle.files) {
assertSafeRelative(file.path);
const target = path.join(root, ...file.path.split("/"));
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
fs.writeFileSync(target, file.bytes, { flag: "wx", mode: 0o600 });
}
const files = bundle.files.map((file, index) => readRegularFile(root, file.path, index === 0 ? MAX_SKILL_BYTES : MAX_BUNDLE_FILE_BYTES, { requireUtf8: index === 0 || file.encoding === "utf-8" }));
const identity = files.map((file) => `${file.path}\0${file.sha256}\0${file.size}`).join("\n");
const bundleHash = sha256(Buffer.from(identity));
if (bundleHash !== bundle.bundleHash) throw new Error("Isolated snapshot hash mismatch");
return { root, bundle: { ...bundle, files, bundleHash } };
} catch (error) {
fs.rmSync(root, { recursive: true, force: true });
throw error;
}
}
function disposeSnapshot(snapshot) {
if (!snapshot?.root || !path.basename(snapshot.root).startsWith("aas-review-snapshot-")) throw new Error("Invalid snapshot cleanup target");
fs.rmSync(snapshot.root, { recursive: true, force: true });
}
module.exports = { assertSafeRelative, bundleMap, discoverBundle, disposeSnapshot, listCanonicalSkillIds, materializeBundle, parseIndexRecords, readRegularFile, sha256, trackedFiles };