Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions npm/agentplugins/lib/public-authoring-contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
"use strict";

// Pure structural codec; authentication and custody remain external.
const c = require("../scripts/dual-authoring-candidate");
const { TextDecoder } = require("node:util");

const INPUT_SCHEMA = "authoring-native-inputs/v1";
const DESCRIPTOR_SCHEMA = "dual-authoring-public-npm/v2";
const INPUT_FILE = "native-inputs.json";
const MODE = "release-cli-contract-v1";
const SCOPE = "six-platform-pair";
const WORKFLOW = ".github/workflows/agentplugins-release.yml";
const MAX_INPUT_BYTES = 1024 * 1024;
const MAX_DESCRIPTOR_BYTES = 64 * 1024;
const MAX_NATIVE_BYTES = 128 * 1024 * 1024;
const PACKAGES = Object.freeze({ agentplugins: "universal-agent-plugins", "plugin-kit-ai": "plugin-kit-ai" });

function fail(label) { throw new Error(`structural consistency only: ${label}`); }
function fixed(value, expected, label) {
if (value !== expected) fail(`${label} mismatch`);
return value;
}
function hash(value, label) {
if (typeof value !== "string" || value.length !== 64 || !/^[0-9a-f]{64}$/.test(value) || /^0+$/.test(value)) fail(`${label} SHA256`);
return value;
}
function positive(value, maximum, label) {
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) fail(`${label} positive bounded integer`);
return value;
}

// Only ordinary own enumerable data fields. Do not silently discard symbols,
// hidden claims or custom prototypes, or invoke accessors/toJSON while encoding.
function fields(value, names, label) {
if (!value || typeof value !== "object" ||
![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail(`${label} data object`);
const own = Reflect.ownKeys(value);
if (own.length !== names.length || own.some(key => typeof key !== "string" || !names.includes(key))) {
fail(`${label} unexpected or missing fields`);
}
for (const key of own) {
const d = Object.getOwnPropertyDescriptor(value, key);
if (!d.enumerable || !("value" in d)) fail(`${label} own enumerable data fields`);
}
c.keys(value, names, label);
}

function identity(value) {
fields(value, ["repository", "commit", "engine_revision", "versions"], "identity");
fields(value.versions, c.PRODUCTS, "versions");
// Bound strings before calling the existing candidate version validator.
for (const product of c.PRODUCTS) {
if (typeof value.versions[product] !== "string" || value.versions[product].length > MAX_INPUT_BYTES ||
/[\r\n]/.test(value.versions[product])) {
fail("version string limit/type");
}
}
// Candidate's $-anchored regex also matches before a final newline; this
// contract requires exactly forty source characters and stable versions.
if (typeof value.commit !== "string" || value.commit.length !== 40) fail("exact source revision length/type");
c.identity(value);
if (/^0+$/.test(value.commit)) fail("nonzero source revision required");
fixed(value.versions["plugin-kit-ai"], "2.0.0", "kit version");
return { repository: value.repository, commit: value.commit, engine_revision: value.engine_revision,
versions: { agentplugins: value.versions.agentplugins, "plugin-kit-ai": value.versions["plugin-kit-ai"] } };
}

function pin(value, file, label) {
fields(value, ["file", "sha256", "size"], label);
return { file: fixed(value.file, file, `${label} file`), sha256: hash(value.sha256, label),
size: positive(value.size, MAX_NATIVE_BYTES, `${label} size`) };
}

function inputs(value) {
fields(value, ["schema", "identity", "authoring_mode", "asset_scope", "candidate_sha256",
"pair_marker_sha256", "products", "preparation", "producer"], "inputs");
fixed(value.schema, INPUT_SCHEMA, "input schema");
const id = identity(value.identity);
fixed(value.authoring_mode, MODE, "authoring mode");
fixed(value.asset_scope, SCOPE, "asset scope");
fields(value.products, c.PRODUCTS, "products");
const products = {}, binaries = new Set();
for (const product of c.PRODUCTS) {
const p = value.products[product], assets = {};
fields(p, ["tag", "manifest_sha256", "checksums_sha256", "assets"], "product");
const tag = (product === "agentplugins" ? "agentplugins-v" : "v") + id.versions[product];
fixed(p.tag, tag, "product tag");
fields(p.assets, c.TARGETS, "assets");
for (const target of c.TARGETS) {
const a = p.assets[target];
fields(a, ["file", "sha256", "size", "binary"], "asset");
const file = c.assetName(product, id.versions[product], target);
const binary = pin(a.binary, c.executableName(product, target), "binary");
const outer = pin({ file: a.file, sha256: a.sha256, size: a.size }, file, "asset");
if (product === "agentplugins" && (outer.sha256 !== binary.sha256 || outer.size !== binary.size)) {
fail("raw agent outer/inner pins disagree");
}
if (binaries.has(binary.sha256)) fail("twelve distinct binary hashes required");
binaries.add(binary.sha256);
assets[target] = { file: outer.file, sha256: outer.sha256, size: outer.size, binary };
}
products[product] = { tag, manifest_sha256: hash(p.manifest_sha256, "manifest"),
checksums_sha256: hash(p.checksums_sha256, "checksums"), assets };
}
const prep = value.preparation, producer = value.producer;
fields(prep, ["sha256", "artifact"], "preparation");
fields(prep.artifact, ["run_id", "run_attempt", "artifact_id", "artifact_sha256"], "preparation artifact");
const a = prep.artifact;
fields(producer, ["workflow", "source", "run_id", "run_attempt"], "producer");
return { schema: INPUT_SCHEMA, identity: id, authoring_mode: MODE, asset_scope: SCOPE,
candidate_sha256: hash(value.candidate_sha256, "candidate"), pair_marker_sha256: hash(value.pair_marker_sha256, "pair marker"),
products, preparation: { sha256: hash(prep.sha256, "preparation"), artifact: {
run_id: positive(a.run_id, Number.MAX_SAFE_INTEGER, "preparation run"),
run_attempt: positive(a.run_attempt, 1000, "preparation attempt"),
artifact_id: positive(a.artifact_id, Number.MAX_SAFE_INTEGER, "preparation artifact ID"),
artifact_sha256: hash(a.artifact_sha256, "preparation artifact") } },
producer: { workflow: fixed(producer.workflow, WORKFLOW, "producer workflow"),
source: fixed(producer.source, id.commit, "producer source"),
run_id: positive(producer.run_id, Number.MAX_SAFE_INTEGER, "producer run"),
run_attempt: positive(producer.run_attempt, 1000, "producer attempt") } };
}

function bytes(value, maximum) {
if (!Buffer.isBuffer(value) || value.length === 0 || value.length > maximum) fail("nonempty bounded Buffer required");
return value;
}
function encoded(value, maximum) {
return bytes(c.encode(value), maximum);
}
function parsed(body, maximum) {
bytes(body, maximum);
const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(body);
// Both fixed contracts have objects only, at most six levels deep. Bound
// nesting before JSON.parse; canonical re-encoding rejects duplicate keys,
// escapes, alternate number spellings, whitespace, BOM and trailing data.
let depth = 0, quoted = false, escaped = false;
for (const ch of text) {
if (quoted) {
if (escaped) escaped = false;
else if (ch === "\\") escaped = true;
else if (ch === '"') quoted = false;
} else if (ch === '"') quoted = true;
else if (ch === "[") fail("arrays are outside the fixed contracts");
else if (ch === "{" && ++depth > 6) fail("object depth limit");
else if (ch === "}") depth--;
}
return JSON.parse(text);
}

/** Encode a complete I object in fixed order. Structural consistency only. */
function encodeInputs(value) { return encoded(inputs(value), MAX_INPUT_BYTES); }

/** Decode canonical I bytes to a fresh data object. Structural consistency only. */
function decodeInputs(body) {
const value = inputs(parsed(body, MAX_INPUT_BYTES));
if (!body.equals(encoded(value, MAX_INPUT_BYTES))) fail("noncanonical input bytes");
return value;
}

function descriptor(value, inputBytes, product) {
if (!c.PRODUCTS.includes(product)) fail("explicit selected product required");
const input = decodeInputs(inputBytes);
fields(value, ["schema", "product", "npm_package", "identity", "authoring_mode", "asset_scope",
"candidate_sha256", "release_manifest_sha256", "input_binding"], "descriptor");
fixed(value.schema, DESCRIPTOR_SCHEMA, "descriptor schema");
fixed(value.product, product, "selected product");
fixed(value.npm_package, PACKAGES[product], "npm package");
const id = identity(value.identity);
if (!c.encode(id).equals(c.encode(input.identity))) fail("descriptor/input identity mismatch");
fields(value.input_binding, ["file", "sha256"], "input binding");
return { schema: DESCRIPTOR_SCHEMA, product, npm_package: PACKAGES[product], identity: id,
authoring_mode: fixed(value.authoring_mode, MODE, "descriptor mode"),
asset_scope: fixed(value.asset_scope, SCOPE, "descriptor scope"),
candidate_sha256: fixed(value.candidate_sha256, input.candidate_sha256, "descriptor candidate"),
release_manifest_sha256: fixed(value.release_manifest_sha256, input.products[product].manifest_sha256, "descriptor manifest"),
input_binding: { file: fixed(value.input_binding.file, INPUT_FILE, "input binding file"),
sha256: fixed(value.input_binding.sha256, c.digest(inputBytes), "exact input bytes digest") } };
}

/** Encode complete v2 data against exact canonical I bytes and a required product.
* Structural consistency only; this does not generate or qualify a package. */
function encodeDescriptor(value, inputBytes, product) {
return encoded(descriptor(value, inputBytes, product), MAX_DESCRIPTOR_BYTES);
}

/** Decode v2 with the same required I bytes/product. Structural consistency only.
* No authentication, signing, acquisition, eligibility or acceptance is implied. */
function decodeDescriptor(body, inputBytes, product) {
const value = descriptor(parsed(body, MAX_DESCRIPTOR_BYTES), inputBytes, product);
if (!body.equals(encoded(value, MAX_DESCRIPTOR_BYTES))) fail("noncanonical descriptor bytes");
return value;
}

// Fixed schema-3 projection, shared with staging. This reconstructs consistency,
// not custody of the absent original candidate, preparation or pair marker.
function projectionBytes(input, product) {
if (!c.PRODUCTS.includes(product)) fail("explicit selected product required");
const id = input.identity;
const manifest = c.encode({ schema_version: 3, status: "CANDIDATE", product, repository: id.repository,
tag: input.products[product].tag, version: id.versions[product], commit: id.commit, engine_revision: id.engine_revision,
versions: id.versions, candidate_sha256: input.candidate_sha256, authoring_mode: input.authoring_mode,
asset_scope: input.asset_scope, assets: input.products[product].assets,
release_eligible: false, platform_acceptance: false, attested: false });
const checksums = Buffer.from([...Object.values(input.products[product].assets).map(a => `${a.sha256} ${a.file}`),
`${c.digest(manifest)} release-manifest.json`].join("\n") + "\n");
return { manifest, checksums };
}

module.exports = Object.freeze({ encodeInputs, decodeInputs, encodeDescriptor, decodeDescriptor, projectionBytes, INPUT_SCHEMA, DESCRIPTOR_SCHEMA, INPUT_FILE, MODE, SCOPE, WORKFLOW, PACKAGES, MAX_INPUT_BYTES, MAX_DESCRIPTOR_BYTES, MAX_NATIVE_BYTES,
checks: Object.freeze({ fail, fixed, hash, positive, fields }) });
121 changes: 121 additions & 0 deletions npm/agentplugins/lib/public-authoring-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"use strict";

// Bounded observations in an owned, quiescent namespace. These path checks do
// not exclude hostile same-UID writers, mount replacement or every host race.
const fs = require("node:fs");
const path = require("node:path");
const crypto = require("node:crypto");
const digest = bytes => crypto.createHash("sha256").update(bytes).digest("hex");
const fail = message => { throw new Error(`public snapshot: ${message}`); };
const directoryKeys = ["dev", "ino", "mode", "uid", "gid"];
const fileKeys = [...directoryKeys, "size", "nlink", "mtimeMs", "ctimeMs"];
const same = (a, b, keys) => keys.every(key => a[key] === b[key]);
const within = (a, b) => { const r = path.relative(b, a); return r === "" || (!r.startsWith(`..${path.sep}`) && r !== ".." && !path.isAbsolute(r)); };
const overlaps = (a, b) => within(a, b) || within(b, a);

function canonical(file) {
if (typeof file !== "string" || !file || file.includes("\0") || !path.isAbsolute(file) ||
path.resolve(file) !== file || file.split(/[\\/]/).some(p => p === "." || p === "..")) fail("canonical absolute path required");
return file;
}
function cancelled(signal) {
if (signal && signal.aborted) { const error = new Error("public snapshot cancelled"); error.code = "ABORT_ERR"; throw error; }
}
function uid() {
if (typeof process.geteuid !== "function" || !fs.constants.O_NOFOLLOW) fail("host ownership/no-follow support required");
return process.geteuid();
}
function directory(name, owner) {
const stat = fs.lstatSync(name);
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(name) !== name ||
(stat.uid !== 0 && stat.uid !== owner) || ((stat.mode & 0o022) && !(stat.mode & 0o1000))) fail("unsafe ancestor");
return stat;
}
function ancestors(name, owner, missing = false) {
canonical(name);
let current = path.parse(name).root;
const pins = [[current, directory(current, owner)]];
for (const part of name.slice(current.length).split(path.sep).filter(Boolean)) {
current = path.join(current, part);
try { pins.push([current, directory(current, owner)]); }
catch (error) { if (missing && error.code === "ENOENT") break; throw error; }
}
return pins;
}
function recheckDirectories(pins, owner) {
for (const [name, pin] of pins) if (!same(directory(name, owner), pin, directoryKeys)) fail("ancestor changed");
}

// Validate the entire configured cache boundary without creating it. Existing
// ancestor aliases are rejected even if the final cache directory is absent.
function publicPlacement(packageRoot, cacheRoot) {
canonical(packageRoot); canonical(cacheRoot);
if (overlaps(packageRoot, cacheRoot)) fail("cache overlaps package");
const owner = uid(), packages = ancestors(packageRoot, owner), cache = ancestors(cacheRoot, owner, true);
const p = packages[packages.length - 1], q = cache[cache.length - 1];
if (q[0] === cacheRoot && p[1].dev === q[1].dev && p[1].ino === q[1].ino) fail("cache aliases package");
return { recheck() { recheckDirectories(packages, owner); recheckDirectories(cache, owner); } };
}

function snapshotPublicFile(file, { kind, maximum, exactSize, protectedRoots = [], signal } = {}) {
cancelled(signal); canonical(file);
if (!["metadata", "local asset"].includes(kind) || !Number.isSafeInteger(maximum) || maximum <= 0 ||
maximum > (kind === "metadata" ? 1024 * 1024 : 128 * 1024 * 1024) ||
(exactSize !== undefined && (!Number.isSafeInteger(exactSize) || exactSize <= 0 || exactSize > maximum))) fail("fixed bounded policy required");
const owner = uid(), parent = path.dirname(file), parents = ancestors(parent, owner);
const extra = [];
if (kind === "local asset") {
const direct = parents[parents.length - 1][1];
if (direct.uid !== owner || (direct.mode & 0o7777) !== 0o700) fail("owned private custody parent required");
for (const root of protectedRoots) {
canonical(root);
if (overlaps(parent, root)) fail("custody overlaps protected root");
const pins = ancestors(root, owner, true), last = pins[pins.length - 1];
if (last[0] === root && direct.dev === last[1].dev && direct.ino === last[1].ino) fail("custody aliases protected root");
extra.push(...pins);
}
}
const before = fs.lstatSync(file);
const regular = stat => stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1 && stat.size > 0 && stat.size <= maximum &&
(exactSize === undefined || stat.size === exactSize) &&
(kind !== "local asset" || (stat.uid === owner && !(stat.mode & 0o7022)));
if (!regular(before)) fail("regular nonempty bounded single-link file required");
let fd, closed = false;
function close() { if (fd !== undefined && !closed) { closed = true; fs.closeSync(fd); } }
function identities() {
cancelled(signal);
if (closed) fail("closed descriptor");
recheckDirectories([...parents, ...extra], owner);
const named = fs.lstatSync(file), opened = fs.fstatSync(fd);
if (!regular(named) || !regular(opened) || !same(before, named, fileKeys) || !same(before, opened, fileKeys)) fail("file identity or metadata changed");
}
function read() {
// Allocate only the admitted initial size plus one overflow detection byte;
// positional reads also make repeated rechecks independent of fd offsets.
const body = Buffer.alloc(before.size + 1);
let offset = 0;
while (offset < body.length) {
cancelled(signal);
const count = fs.readSync(fd, body, offset, Math.min(64 * 1024, body.length - offset), offset);
if (!Number.isSafeInteger(count) || count < 0 || count > Math.min(64 * 1024, body.length - offset)) fail("invalid bounded read result");
if (count === 0) break;
offset += count;
}
if (offset !== before.size) fail("short read or growth");
return body.subarray(0, offset);
}
try {
fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
identities();
const bytes = read();
identities();
const pin = digest(bytes);
return { get bytes() { return Buffer.from(bytes); }, close,
recheck() { identities(); const current = read(); identities(); if (digest(current) !== pin) fail("file contents changed"); } };
} catch (primary) {
try { close(); } catch (error) { throw new AggregateError([primary, error], "public snapshot read and close failed"); }
throw primary;
}
}

module.exports = { snapshotPublicFile, publicPlacement };
Loading
Loading