📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-07-18 00:02:59 +00:00
parent 82f7c6e56a
commit 47ce7f78dc
1446 changed files with 141041 additions and 6442 deletions
@@ -0,0 +1,177 @@
"use strict";
const { hostConfigError } = require("./errors");
const KNOWN_KEYS = ["command", "args", "env"];
function decodeUtf8(bytes) {
if (bytes.length > 1024 * 1024) throw hostConfigError("AAS_ADAPTER_CONFIG_TOO_LARGE", "invalidInput");
try {
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
if (text.charCodeAt(0) === 0xfeff) throw new Error("bom");
return text;
} catch {
throw hostConfigError("AAS_ADAPTER_CONFIG_ENCODING_INVALID", "invalidInput");
}
}
function parseJsonAst(text) {
let cursor = 0;
function fail() { throw hostConfigError("AAS_ADAPTER_CLAUDE_JSON_INVALID", "invalidInput"); }
function whitespace() { while (/[\u0009\u000a\u000d\u0020]/.test(text[cursor] || "")) cursor += 1; }
function string() {
const start = cursor;
if (text[cursor] !== '"') fail();
cursor += 1;
while (cursor < text.length) {
if (text[cursor] === '"') {
cursor += 1;
try { return { value: JSON.parse(text.slice(start, cursor)), start, end: cursor }; } catch { fail(); }
}
if (text[cursor] === "\\") cursor += 2;
else cursor += 1;
}
fail();
}
function value(depth = 0) {
if (depth > 32) fail();
whitespace();
const start = cursor;
if (text[cursor] === "{") return object(depth + 1);
if (text[cursor] === "[") return array(depth + 1);
if (text[cursor] === '"') {
const node = string();
return { type: "string", ...node };
}
while (cursor < text.length && !/[\s,}\]]/.test(text[cursor])) cursor += 1;
if (cursor === start) fail();
try { return { type: "primitive", value: JSON.parse(text.slice(start, cursor)), start, end: cursor }; } catch { fail(); }
}
function array(depth) {
const start = cursor;
const items = [];
cursor += 1;
whitespace();
if (text[cursor] === "]") { cursor += 1; return { type: "array", value: [], items, start, end: cursor }; }
while (cursor < text.length) {
const item = value(depth);
items.push(item);
whitespace();
if (text[cursor] === "]") { cursor += 1; return { type: "array", value: items.map((entry) => entry.value), items, start, end: cursor }; }
if (text[cursor] !== ",") fail();
cursor += 1;
}
fail();
}
function object(depth) {
const start = cursor;
const properties = [];
const keys = new Set();
const result = {};
cursor += 1;
whitespace();
if (text[cursor] === "}") { cursor += 1; return { type: "object", value: result, properties, start, end: cursor }; }
while (cursor < text.length) {
whitespace();
const key = string();
if (keys.has(key.value)) throw hostConfigError("AAS_ADAPTER_CLAUDE_JSON_DUPLICATE_KEY", "invalidInput", { key: key.value });
keys.add(key.value);
whitespace();
if (text[cursor] !== ":") fail();
cursor += 1;
const child = value(depth);
properties.push({ key: key.value, keyNode: key, valueNode: child });
result[key.value] = child.value;
whitespace();
if (text[cursor] === "}") { cursor += 1; return { type: "object", value: result, properties, start, end: cursor }; }
if (text[cursor] !== ",") fail();
cursor += 1;
}
fail();
}
const root = value(0);
whitespace();
if (cursor !== text.length || root.type !== "object") fail();
return root;
}
function property(node, key) {
return node.properties.find((entry) => entry.key === key);
}
function indentation(text) {
const match = text.match(/\n([ \t]+)"/);
return match ? match[1] : " ";
}
function lineIndent(text, offset) {
const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
return (text.slice(lineStart, offset).match(/^[ \t]*/) || [""])[0];
}
function formatValue(value, continuationIndent, indentUnit) {
return JSON.stringify(value, null, indentUnit).split("\n").map((line, index) => index === 0 ? line : `${continuationIndent}${line}`).join("\n");
}
function replaceRange(text, start, end, replacement) {
return `${text.slice(0, start)}${replacement}${text.slice(end)}`;
}
function insertProperty(text, node, key, value, indentUnit) {
const baseIndent = lineIndent(text, node.start);
const childIndent = `${baseIndent}${indentUnit}`;
const rendered = `${JSON.stringify(key)}: ${formatValue(value, childIndent, indentUnit)}`;
if (node.properties.length === 0) {
return replaceRange(text, node.start, node.end, `{\n${childIndent}${rendered}\n${baseIndent}}`);
}
const last = node.properties.at(-1).valueNode;
return replaceRange(text, last.end, last.end, `,\n${childIndent}${rendered}`);
}
function sameValue(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
function inspectTree(root) {
const mcp = property(root, "mcpServers");
if (mcp && mcp.valueNode.type !== "object") throw hostConfigError("AAS_ADAPTER_CLAUDE_SECTION_AMBIGUOUS", "invalidInput", { key: "mcpServers" });
const aas = mcp ? property(mcp.valueNode, "aas") : null;
if (aas && aas.valueNode.type !== "object") throw hostConfigError("AAS_ADAPTER_CLAUDE_SECTION_AMBIGUOUS", "invalidInput", { key: "aas" });
return {
mcp: mcp ? mcp.valueNode : null,
aas: aas ? aas.valueNode : null,
inspection: {
sectionPresent: Boolean(aas),
configured: Boolean(aas) && KNOWN_KEYS.every((key) => Object.hasOwn(aas.valueNode.value, key)),
unknownKeys: aas ? Object.keys(aas.valueNode.value).filter((key) => !KNOWN_KEYS.includes(key)).sort() : [],
},
};
}
function buildClaudeText(bytes, server) {
const source = decodeUtf8(bytes);
const text = source.trim() === "" ? "{}\n" : source;
const root = parseJsonAst(text);
const { mcp, aas, inspection } = inspectTree(root);
const changedPaths = KNOWN_KEYS.filter((key) => !aas || !Object.hasOwn(aas.value, key) || !sameValue(aas.value[key], server[key])).map((key) => `mcpServers.aas.${key}`);
if (changedPaths.length === 0) return { text, inspection, changedPaths };
const indentUnit = indentation(text);
let nextText;
if (aas) {
const nextAas = { ...aas.value, command: server.command, args: server.args, env: server.env };
nextText = replaceRange(text, aas.start, aas.end, formatValue(nextAas, lineIndent(text, aas.start), indentUnit));
} else if (mcp) {
nextText = insertProperty(text, mcp, "aas", server, indentUnit);
} else {
nextText = insertProperty(text, root, "mcpServers", { aas: server }, indentUnit);
}
return { text: nextText, inspection, changedPaths };
}
function inspectClaudeBytes(bytes) {
const source = decodeUtf8(bytes);
const root = parseJsonAst(source.trim() === "" ? "{}" : source);
return inspectTree(root).inspection;
}
module.exports = { buildClaudeText, inspectClaudeBytes, parseJsonAst };
@@ -0,0 +1,183 @@
"use strict";
const { hostConfigError } = require("./errors");
const SECTION = "mcp_servers.aas";
const KNOWN_KEYS = ["command", "args", "enabled"];
function decodeUtf8(bytes) {
if (bytes.length > 1024 * 1024) throw hostConfigError("AAS_ADAPTER_CONFIG_TOO_LARGE", "invalidInput");
try {
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
if (text.charCodeAt(0) === 0xfeff) throw new Error("bom");
return text;
} catch {
throw hostConfigError("AAS_ADAPTER_CONFIG_ENCODING_INVALID", "invalidInput");
}
}
function stripInlineComment(value) {
let quoted = false;
let escaped = false;
let depth = 0;
for (let index = 0; index < value.length; index += 1) {
const character = value[index];
if (quoted) {
if (escaped) escaped = false;
else if (character === "\\") escaped = true;
else if (character === '"') quoted = false;
continue;
}
if (character === '"') quoted = true;
else if (character === "[") depth += 1;
else if (character === "]") depth -= 1;
else if (character === "#" && depth === 0) return { value: value.slice(0, index).trim(), comment: value.slice(index) };
}
return { value: value.trim(), comment: "" };
}
function findAssignment(line) {
let quoted = false;
let escaped = false;
for (let index = 0; index < line.length; index += 1) {
const character = line[index];
if (quoted) {
if (escaped) escaped = false;
else if (character === "\\") escaped = true;
else if (character === '"') quoted = false;
} else if (character === '"') quoted = true;
else if (character === "=") return { key: line.slice(0, index).trim(), raw: line.slice(index + 1), indent: line.match(/^\s*/)[0] };
}
return null;
}
function parseKnownValue(key, raw) {
const source = stripInlineComment(raw).value;
try {
if (key === "command") {
const value = JSON.parse(source);
if (typeof value !== "string") throw new Error("not string");
return value;
}
if (key === "args") {
const value = JSON.parse(source);
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error("not string array");
return value;
}
if (key === "enabled" && /^(true|false)$/.test(source)) return source === "true";
} catch {
// Convert all unsupported TOML forms into a stable adapter error below.
}
throw hostConfigError("AAS_ADAPTER_CODEX_SECTION_AMBIGUOUS", "invalidInput", { key });
}
function parseCodexText(text) {
const newline = text.includes("\r\n") ? "\r\n" : "\n";
const terminalNewline = text.endsWith("\n");
const lines = text.length === 0 ? [] : text.split(/\r?\n/);
if (terminalNewline) lines.pop();
let currentSection = "";
let exactStart = -1;
let exactEnd = lines.length;
let exactCount = 0;
const values = {};
const lineIndexes = {};
const unknownKeys = [];
for (let index = 0; index < lines.length; index += 1) {
const trimmed = lines[index].trim();
const header = trimmed.match(/^\[([^\]]+)\]\s*(?:#.*)?$/);
const arrayHeader = trimmed.match(/^\[\[([^\]]+)\]\]\s*(?:#.*)?$/);
if (arrayHeader) {
const name = arrayHeader[1].trim();
if (name.includes("mcp_servers") && name.includes("aas")) throw hostConfigError("AAS_ADAPTER_CODEX_SECTION_AMBIGUOUS", "invalidInput");
currentSection = name;
continue;
}
if (header) {
const name = header[1].trim();
if (currentSection === SECTION && exactEnd === lines.length) exactEnd = index;
if (name === SECTION) {
exactCount += 1;
exactStart = index;
} else if (name.includes("mcp_servers") && name.includes("aas")) {
throw hostConfigError("AAS_ADAPTER_CODEX_SECTION_AMBIGUOUS", "invalidInput");
}
currentSection = name;
continue;
}
if (trimmed === "" || trimmed.startsWith("#")) continue;
const assignment = findAssignment(lines[index]);
if (!assignment) continue;
const key = assignment.key;
if (currentSection !== SECTION) {
if (key === "mcp_servers.aas" || key.startsWith("mcp_servers.aas.") || (currentSection === "mcp_servers" && (key === "aas" || key.startsWith("aas.")))) {
throw hostConfigError("AAS_ADAPTER_CODEX_SECTION_AMBIGUOUS", "invalidInput");
}
continue;
}
if (KNOWN_KEYS.includes(key)) {
if (Object.hasOwn(lineIndexes, key)) throw hostConfigError("AAS_ADAPTER_CODEX_SECTION_AMBIGUOUS", "invalidInput", { key });
values[key] = parseKnownValue(key, assignment.raw);
lineIndexes[key] = index;
} else {
unknownKeys.push(key);
}
}
if (currentSection === SECTION && exactEnd === lines.length) exactEnd = lines.length;
if (exactCount > 1) throw hostConfigError("AAS_ADAPTER_CODEX_SECTION_AMBIGUOUS", "invalidInput");
return { newline, terminalNewline, lines, present: exactCount === 1, start: exactStart, end: exactEnd, values, lineIndexes, unknownKeys };
}
function encodeValue(key, value) {
if (key === "command") return JSON.stringify(value);
if (key === "args") return `[${value.map((entry) => JSON.stringify(entry)).join(", ")}]`;
return value ? "true" : "false";
}
function sameValue(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
function buildCodexText(bytes, server) {
const text = decodeUtf8(bytes);
const parsed = parseCodexText(text);
const lines = [...parsed.lines];
if (!parsed.present) {
if (lines.length > 0 && lines.at(-1).trim() !== "") lines.push("");
lines.push(`[${SECTION}]`);
for (const key of KNOWN_KEYS) lines.push(`${key} = ${encodeValue(key, server[key])}`);
} else {
for (const key of KNOWN_KEYS) {
if (!Object.hasOwn(parsed.lineIndexes, key)) continue;
if (sameValue(parsed.values[key], server[key])) continue;
const index = parsed.lineIndexes[key];
const assignment = findAssignment(lines[index]);
const { comment } = stripInlineComment(assignment.raw);
lines[index] = `${assignment.indent}${key} = ${encodeValue(key, server[key])}${comment ? ` ${comment}` : ""}`;
}
const missing = KNOWN_KEYS.filter((key) => !Object.hasOwn(parsed.lineIndexes, key));
if (missing.length > 0) lines.splice(parsed.end, 0, ...missing.map((key) => `${key} = ${encodeValue(key, server[key])}`));
}
const nextText = `${lines.join(parsed.newline)}${lines.length > 0 && (parsed.terminalNewline || text.length === 0) ? parsed.newline : ""}`;
return {
text: nextText,
inspection: {
sectionPresent: parsed.present,
configured: parsed.present && KNOWN_KEYS.every((key) => Object.hasOwn(parsed.values, key)),
unknownKeys: [...parsed.unknownKeys].sort(),
},
changedPaths: KNOWN_KEYS.filter((key) => !Object.hasOwn(parsed.values, key) || !sameValue(parsed.values[key], server[key])).map((key) => `mcp_servers.aas.${key}`),
};
}
function inspectCodexBytes(bytes) {
const parsed = parseCodexText(decodeUtf8(bytes));
return {
sectionPresent: parsed.present,
configured: parsed.present && KNOWN_KEYS.every((key) => Object.hasOwn(parsed.values, key)),
unknownKeys: [...parsed.unknownKeys].sort(),
};
}
module.exports = { buildCodexText, inspectCodexBytes, parseCodexText };
@@ -0,0 +1,17 @@
"use strict";
class HostConfigError extends Error {
constructor(code, category = "invalidInput", details = {}) {
super(code);
this.name = "HostConfigError";
this.code = code;
this.category = category;
this.details = details;
}
}
function hostConfigError(code, category, details) {
return new HostConfigError(code, category, details);
}
module.exports = { HostConfigError, hostConfigError };
@@ -0,0 +1,343 @@
"use strict";
const crypto = require("node:crypto");
const fsp = require("node:fs/promises");
const path = require("node:path");
const { canonicalJson } = require("../canonical-json");
const { buildClaudeText, inspectClaudeBytes } = require("./claude");
const { buildCodexText, inspectCodexBytes } = require("./codex");
const { HostConfigError, hostConfigError } = require("./errors");
const {
assertExplicitAbsolutePath,
assertOwned,
assertSafeDirectory,
assertWindowsPrivatePath,
copyWindowsAcl,
currentUid,
digest,
fsyncDirectory,
hardenWindowsPrivatePath,
inspectRegularFile,
sameIdentity,
writeExclusiveSynced,
} = require("./safety");
const { normalizeServer } = require("./values");
const PRIVATE = new WeakMap();
const HOSTS = new Set(["codex", "claude"]);
const SCOPES = new Set(["project", "user"]);
function validateHostScope(host, scope) {
if (!HOSTS.has(host)) throw hostConfigError("AAS_ADAPTER_HOST_UNSUPPORTED", "invalidInput", { host });
if (!SCOPES.has(scope)) throw hostConfigError("AAS_ADAPTER_SCOPE_INVALID", "invalidInput", { scope });
}
function parentIdentity(parent) {
const stat = parent.stat;
return { dev: stat.dev, ino: stat.ino, uid: stat.uid, gid: stat.gid, mode: stat.mode & 0o7777 };
}
function publicInspection(host, scope, snapshot, inspection) {
return Object.freeze({
schemaVersion: 1,
host,
scope,
configPath: snapshot.path,
exists: snapshot.exists,
digest: snapshot.digest,
mode: snapshot.identity?.mode ?? null,
sectionPresent: inspection.sectionPresent,
configured: inspection.configured,
unknownKeys: Object.freeze([...inspection.unknownKeys]),
});
}
async function inspectHostConfig({ host, scope, configPath }) {
validateHostScope(host, scope);
const snapshot = await inspectRegularFile(configPath);
const inspection = host === "codex" ? inspectCodexBytes(snapshot.bytes) : inspectClaudeBytes(snapshot.bytes);
return publicInspection(host, scope, snapshot, inspection);
}
async function buildPatch({ host, scope, configPath, server }) {
validateHostScope(host, scope);
const desired = normalizeServer(host, server);
const snapshot = await inspectRegularFile(configPath);
const built = host === "codex" ? buildCodexText(snapshot.bytes, desired) : buildClaudeText(snapshot.bytes, desired);
const nextBytes = Buffer.from(built.text, "utf8");
const nextDigest = digest(nextBytes);
const changed = snapshot.digest !== nextDigest;
const patch = {
schemaVersion: 1,
host,
scope,
configPath: snapshot.path,
status: changed ? "changesProposed" : "alreadyConfigured",
changed,
exists: snapshot.exists,
currentDigest: snapshot.digest,
nextDigest,
redactedDiff: Object.freeze({
schemaVersion: 1,
changedPaths: Object.freeze([...built.changedPaths].sort()),
valuesRedacted: true,
envValuesRedacted: true,
}),
inspection: publicInspection(host, scope, snapshot, built.inspection),
};
Object.freeze(patch);
PRIVATE.set(patch, {
currentBytes: snapshot.bytes,
nextBytes,
identity: snapshot.identity,
parentIdentity: parentIdentity(snapshot.parent),
});
return patch;
}
function previewPatch(patch) {
if (!PRIVATE.has(patch)) throw hostConfigError("AAS_ADAPTER_PATCH_INVALID", "invalidInput");
return patch;
}
async function acquireLock(lockPath, payload) {
try {
const handle = await fsp.open(lockPath, "wx", 0o600);
try {
hardenWindowsPrivatePath(lockPath, false);
await handle.writeFile(`${canonicalJson(payload)}\n`);
await handle.chmod(0o600);
await handle.sync();
} finally {
await handle.close();
}
} catch (error) {
if (error.code === "EEXIST") throw hostConfigError("AAS_ADAPTER_LOCKED", "conflict");
throw error;
}
}
async function releaseLock(lockPath) {
try { await fsp.unlink(lockPath); } catch (error) { if (error.code !== "ENOENT") throw error; }
await fsyncDirectory(path.dirname(lockPath));
}
async function ensureBackupDirectory(backupDirectory) {
const absolute = assertExplicitAbsolutePath(backupDirectory, "AAS_ADAPTER_BACKUP_PATH_INVALID");
const existing = assertSafeDirectory(absolute, { allowMissing: true });
if (existing.exists) {
if (process.platform !== "win32" && (existing.stat.mode & 0o077) !== 0) throw hostConfigError("AAS_ADAPTER_BACKUP_DIRECTORY_PERMISSIONS", "filesystem");
if (process.platform === "win32") assertWindowsPrivatePath(existing.path);
return absolute;
}
assertSafeDirectory(path.dirname(absolute));
try {
await fsp.mkdir(absolute, { mode: 0o700 });
} catch (error) {
if (error.code !== "EEXIST") throw error;
}
const created = assertSafeDirectory(absolute);
await fsp.chmod(created.path, 0o700);
if (process.platform === "win32") hardenWindowsPrivatePath(created.path, true);
await fsyncDirectory(path.dirname(created.path));
return created.path;
}
function safeTimestamp(value) {
const date = value instanceof Date ? value : new Date(value ?? Date.now());
if (!Number.isFinite(date.getTime())) throw hostConfigError("AAS_ADAPTER_TIMESTAMP_INVALID", "invalidInput");
return date.toISOString().replace(/[:.]/g, "-");
}
function configKey(configPath) {
return digest(Buffer.from(configPath)).slice("sha256-".length, "sha256-".length + 24);
}
async function createBackup({ patch, snapshot, backupDirectory, retention, now }) {
if (!Number.isInteger(retention) || retention < 1 || retention > 100) throw hostConfigError("AAS_ADAPTER_RETENTION_INVALID", "invalidInput");
const directory = await ensureBackupDirectory(backupDirectory);
const key = configKey(patch.configPath);
const stem = `${key}-${safeTimestamp(now)}-${snapshot.digest.slice(-12)}`;
const backupFile = `${stem}.bak`;
const metadataFile = `${stem}.json`;
const backupPath = path.join(directory, backupFile);
const metadataPath = path.join(directory, metadataFile);
try {
await writeExclusiveSynced(backupPath, snapshot.bytes, 0o600);
const metadata = {
schemaVersion: 1,
host: patch.host,
scope: patch.scope,
configKey: key,
configBasename: path.basename(patch.configPath),
backupFile,
sourceDigest: snapshot.digest,
sourceMode: snapshot.identity.mode,
sourceUid: snapshot.identity.uid,
sourceGid: snapshot.identity.gid,
createdAt: (now instanceof Date ? now : new Date(now ?? Date.now())).toISOString(),
retention: { maxEntries: retention, enforcement: "explicit-cleanup" },
};
await writeExclusiveSynced(metadataPath, Buffer.from(`${canonicalJson(metadata)}\n`), 0o600);
await fsyncDirectory(directory);
return Object.freeze({ backupPath, metadataPath, sourceDigest: snapshot.digest });
} catch (error) {
await Promise.allSettled([fsp.unlink(backupPath), fsp.unlink(metadataPath)]);
throw error;
}
}
async function assertSnapshotUnchanged(patch, internal) {
const current = await inspectRegularFile(patch.configPath);
if (current.exists !== patch.exists || current.digest !== patch.currentDigest || !sameIdentity(current.identity, internal.identity)) {
throw hostConfigError("AAS_ADAPTER_CONFIG_CHANGED", "conflict");
}
if (!sameIdentity(parentIdentity(current.parent), internal.parentIdentity)) {
throw hostConfigError("AAS_ADAPTER_DIRECTORY_CHANGED", "conflict");
}
return current;
}
async function applyHostConfigPatch({ patch, approved = false, backupDirectory, retention = 5, now } = {}) {
const internal = PRIVATE.get(patch);
if (!internal) throw hostConfigError("AAS_ADAPTER_PATCH_INVALID", "invalidInput");
if (!approved) throw hostConfigError("AAS_ADAPTER_APPROVAL_REQUIRED", "policy");
if (!patch.changed) return Object.freeze({ status: "alreadyConfigured", configDigest: patch.currentDigest, backup: null });
if (patch.exists && typeof backupDirectory !== "string") throw hostConfigError("AAS_ADAPTER_BACKUP_REQUIRED", "policy");
const directory = path.dirname(patch.configPath);
const lockPath = path.join(directory, `.${path.basename(patch.configPath)}.aas.lock`);
const lockPayload = { schemaVersion: 1, pid: process.pid, createdAt: new Date().toISOString(), configKey: configKey(patch.configPath) };
await acquireLock(lockPath, lockPayload);
let stagePath;
try {
let current = await assertSnapshotUnchanged(patch, internal);
const backup = current.exists ? await createBackup({ patch, snapshot: current, backupDirectory, retention, now }) : null;
stagePath = path.join(directory, `.${path.basename(patch.configPath)}.aas-stage-${process.pid}-${crypto.randomBytes(12).toString("hex")}`);
const targetMode = current.exists ? current.identity.mode : 0o600;
const handle = await fsp.open(stagePath, "wx", targetMode);
try {
// A newly created Windows file inherits its parent DACL regardless of
// the POSIX mode. Install the approved target ACL while the stage is
// still empty, before writing configuration bytes.
if (process.platform === "win32") {
if (current.exists) copyWindowsAcl(patch.configPath, stagePath);
else hardenWindowsPrivatePath(stagePath, false);
}
await handle.writeFile(internal.nextBytes);
await handle.chmod(targetMode);
if (current.exists && currentUid() !== null) await handle.chown(current.identity.uid, current.identity.gid);
await handle.sync();
} finally {
await handle.close();
}
current = await assertSnapshotUnchanged(patch, internal);
await fsp.rename(stagePath, patch.configPath);
stagePath = null;
await fsyncDirectory(directory);
const written = await inspectRegularFile(patch.configPath, { allowMissing: false });
if (written.digest !== patch.nextDigest || (process.platform !== "win32" && written.identity.mode !== targetMode)) {
throw hostConfigError("AAS_ADAPTER_WRITE_VERIFICATION_FAILED", "execution");
}
return Object.freeze({ status: "applied", configDigest: written.digest, backup });
} finally {
if (stagePath) await fsp.unlink(stagePath).catch((error) => { if (error.code !== "ENOENT") throw error; });
await releaseLock(lockPath);
}
}
async function readBackupRecords({ backupDirectory, configPath, keep }) {
if (!Number.isInteger(keep) || keep < 0 || keep > 100) throw hostConfigError("AAS_ADAPTER_RETENTION_INVALID", "invalidInput");
const backup = assertSafeDirectory(backupDirectory);
if (process.platform !== "win32" && (backup.stat.mode & 0o077) !== 0) throw hostConfigError("AAS_ADAPTER_BACKUP_DIRECTORY_PERMISSIONS", "filesystem");
const directory = backup.path;
const absoluteConfig = assertExplicitAbsolutePath(configPath);
const key = configKey(absoluteConfig);
const names = (await fsp.readdir(directory)).filter((name) => name.startsWith(`${key}-`) && name.endsWith(".json")).sort().reverse();
const records = [];
for (const name of names) {
const metadataPath = path.join(directory, name);
const stat = await fsp.lstat(metadataPath);
if (stat.isSymbolicLink() || !stat.isFile() || (process.platform !== "win32" && (stat.mode & 0o777) !== 0o600)) throw hostConfigError("AAS_ADAPTER_BACKUP_UNSAFE", "filesystem");
assertOwned(stat, undefined, metadataPath);
if (process.platform === "win32") assertWindowsPrivatePath(metadataPath);
const bytes = await fsp.readFile(metadataPath);
if (bytes.length > 32 * 1024) throw hostConfigError("AAS_ADAPTER_BACKUP_UNSAFE", "filesystem");
let metadata;
try { metadata = JSON.parse(bytes.toString("utf8")); } catch { throw hostConfigError("AAS_ADAPTER_BACKUP_UNSAFE", "filesystem"); }
if (metadata.configKey !== key || typeof metadata.backupFile !== "string" || path.basename(metadata.backupFile) !== metadata.backupFile) {
throw hostConfigError("AAS_ADAPTER_BACKUP_UNSAFE", "filesystem");
}
const backupPath = path.join(directory, metadata.backupFile);
const backupStat = await fsp.lstat(backupPath);
if (backupStat.isSymbolicLink() || !backupStat.isFile() || (process.platform !== "win32" && (backupStat.mode & 0o777) !== 0o600)) throw hostConfigError("AAS_ADAPTER_BACKUP_UNSAFE", "filesystem");
assertOwned(backupStat, undefined, backupPath);
if (process.platform === "win32") assertWindowsPrivatePath(backupPath);
const backupBytes = await fsp.readFile(backupPath);
if (digest(backupBytes) !== metadata.sourceDigest) throw hostConfigError("AAS_ADAPTER_BACKUP_UNSAFE", "filesystem");
records.push({
metadata,
metadataPath,
backupPath,
approvalRecord: {
metadataNameDigest: digest(Buffer.from(name)),
backupNameDigest: digest(Buffer.from(metadata.backupFile)),
metadataDigest: digest(bytes),
backupDigest: digest(backupBytes),
},
});
}
const approvalPayload = {
schemaVersion: 1,
action: "mcp.backups.cleanup",
backupDirectoryDigest: digest(Buffer.from(directory)),
configPathDigest: digest(Buffer.from(absoluteConfig)),
keep,
records: records.map((record) => record.approvalRecord),
};
return { directory, key, records, approvalDigest: digest(Buffer.from(canonicalJson(approvalPayload))) };
}
async function previewBackupCleanup({ backupDirectory, configPath, keep } = {}) {
const inspected = await readBackupRecords({ backupDirectory, configPath, keep });
return Object.freeze({
schemaVersion: 1,
status: inspected.records.length > keep ? "changesProposed" : "nothingToClean",
approvalDigest: inspected.approvalDigest,
retained: Math.min(keep, inspected.records.length),
removeCount: Math.max(0, inspected.records.length - keep),
});
}
async function cleanupBackups({ backupDirectory, configPath, keep, approved = false, approvalDigest } = {}) {
if (!approved && !approvalDigest) throw hostConfigError("AAS_ADAPTER_APPROVAL_REQUIRED", "policy");
const preview = await readBackupRecords({ backupDirectory, configPath, keep });
if (approvalDigest && approvalDigest !== preview.approvalDigest) throw hostConfigError("AAS_ADAPTER_APPROVAL_MISMATCH", "policy");
const { directory, key } = preview;
const lockPath = path.join(directory, `.cleanup-${key}.lock`);
await acquireLock(lockPath, { schemaVersion: 1, pid: process.pid, configKey: key, createdAt: new Date().toISOString() });
try {
const locked = await readBackupRecords({ backupDirectory, configPath, keep });
if (locked.approvalDigest !== preview.approvalDigest) throw hostConfigError("AAS_ADAPTER_BACKUP_CHANGED", "conflict");
const removed = [];
for (const record of locked.records.slice(keep)) {
await fsp.unlink(record.backupPath);
await fsp.unlink(record.metadataPath);
removed.push(path.basename(record.backupPath));
}
await fsyncDirectory(directory);
return Object.freeze({ status: "cleaned", retained: Math.min(keep, locked.records.length), removed: Object.freeze(removed) });
} finally {
await releaseLock(lockPath);
}
}
module.exports = {
HostConfigError,
applyHostConfigPatch,
buildPatch,
cleanupBackups,
inspectHostConfig,
previewBackupCleanup,
previewPatch,
};
@@ -0,0 +1,207 @@
"use strict";
const crypto = require("node:crypto");
const { spawnSync } = require("node:child_process");
const fs = require("node:fs");
const fsp = require("node:fs/promises");
const path = require("node:path");
const { fsyncDirectoryAsync } = require("../durability");
const { hostConfigError } = require("./errors");
function digest(bytes) {
return `sha256-${crypto.createHash("sha256").update(bytes).digest("hex")}`;
}
function currentUid() {
return typeof process.getuid === "function" ? process.getuid() : null;
}
function runWindowsAcl(script, filePath) {
const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, filePath], {
encoding: "utf8",
windowsHide: true,
timeout: 15000,
maxBuffer: 64 * 1024,
});
if (result.status !== 0 || result.error) {
throw hostConfigError("AAS_ADAPTER_WINDOWS_ACL_FAILED", "filesystem", { status: result.status ?? null });
}
return result.stdout.trim();
}
function windowsAclSnapshot(filePath) {
const script = [
"$ErrorActionPreference='Stop'",
"$p=$args[0]",
"$me=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
"$a=Get-Acl -LiteralPath $p",
"$owner=(New-Object Security.Principal.NTAccount($a.Owner)).Translate([Security.Principal.SecurityIdentifier]).Value",
"$rules=@($a.Access | ForEach-Object { $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value + '|' + $_.AccessControlType + '|' + $_.IsInherited })",
"@{current=$me;owner=$owner;protected=$a.AreAccessRulesProtected;rules=$rules}|ConvertTo-Json -Compress",
].join(";");
let snapshot;
try { snapshot = JSON.parse(runWindowsAcl(script, filePath)); } catch (cause) {
if (cause && cause.code) throw cause;
throw hostConfigError("AAS_ADAPTER_WINDOWS_ACL_FAILED", "filesystem");
}
return snapshot;
}
function assertWindowsOwned(filePath) {
if (process.platform !== "win32") return;
const snapshot = windowsAclSnapshot(filePath);
if (snapshot.owner !== snapshot.current) throw hostConfigError("AAS_ADAPTER_OWNERSHIP_MISMATCH", "filesystem");
}
function assertWindowsPrivatePath(filePath) {
if (process.platform !== "win32") return;
const snapshot = windowsAclSnapshot(filePath);
const rules = Array.isArray(snapshot.rules) ? snapshot.rules : (snapshot.rules ? [snapshot.rules] : []);
if (snapshot.owner !== snapshot.current || snapshot.protected !== true || rules.length !== 1
|| rules[0] !== `${snapshot.current}|Allow|False`) {
throw hostConfigError("AAS_ADAPTER_WINDOWS_ACL_UNSAFE", "filesystem");
}
}
function hardenWindowsPrivatePath(filePath, directory = false) {
if (process.platform !== "win32") return;
const script = [
"$ErrorActionPreference='Stop'",
"$p=$args[0]",
"$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User",
"$acl=Get-Acl -LiteralPath $p",
"$acl.SetAccessRuleProtection($true,$false)",
"@($acl.Access)|ForEach-Object{$acl.RemoveAccessRuleAll($_)}",
`$inherit=${directory ? "[Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'" : "[Security.AccessControl.InheritanceFlags]::None"}`,
"$rule=New-Object Security.AccessControl.FileSystemAccessRule($sid,[Security.AccessControl.FileSystemRights]::FullControl,$inherit,[Security.AccessControl.PropagationFlags]::None,[Security.AccessControl.AccessControlType]::Allow)",
"$acl.SetOwner($sid)",
"$acl.SetAccessRule($rule)",
"Set-Acl -LiteralPath $p -AclObject $acl",
].join(";");
runWindowsAcl(script, filePath);
assertWindowsPrivatePath(filePath);
}
function copyWindowsAcl(sourcePath, destinationPath) {
if (process.platform !== "win32") return;
const script = "$ErrorActionPreference='Stop';$source=$args[0];$destination=$args[1];$acl=Get-Acl -LiteralPath $source;Set-Acl -LiteralPath $destination -AclObject $acl";
const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, sourcePath, destinationPath], {
encoding: "utf8", windowsHide: true, timeout: 15000, maxBuffer: 64 * 1024,
});
if (result.status !== 0 || result.error) throw hostConfigError("AAS_ADAPTER_WINDOWS_ACL_FAILED", "filesystem", { status: result.status ?? null });
assertWindowsOwned(destinationPath);
}
function assertExplicitAbsolutePath(filePath, code = "AAS_ADAPTER_PATH_INVALID") {
if (typeof filePath !== "string" || filePath.length === 0 || filePath.includes("\0") || !path.isAbsolute(filePath)) {
throw hostConfigError(code, "invalidInput");
}
return path.normalize(filePath);
}
function assertOwned(stat, expectedUid = currentUid(), filePath = null) {
if (expectedUid !== null && stat.uid !== expectedUid) {
throw hostConfigError("AAS_ADAPTER_OWNERSHIP_MISMATCH", "filesystem", { expectedUid, actualUid: stat.uid });
}
if (process.platform === "win32" && filePath) assertWindowsOwned(filePath);
}
function assertSafeDirectory(directoryPath, options = {}) {
const absolute = assertExplicitAbsolutePath(directoryPath, "AAS_ADAPTER_DIRECTORY_PATH_INVALID");
let stat;
try {
stat = fs.lstatSync(absolute);
} catch (error) {
if (error.code === "ENOENT" && options.allowMissing) return { path: absolute, exists: false };
throw error;
}
if (stat.isSymbolicLink() || !stat.isDirectory()) {
throw hostConfigError("AAS_ADAPTER_DIRECTORY_UNSAFE", "filesystem");
}
assertOwned(stat, options.expectedUid, absolute);
return { path: absolute, exists: true, stat };
}
async function inspectRegularFile(filePath, options = {}) {
const absolute = assertExplicitAbsolutePath(filePath);
const parent = assertSafeDirectory(path.dirname(absolute), { expectedUid: options.expectedUid });
let stat;
try {
stat = await fsp.lstat(absolute);
} catch (error) {
if (error.code === "ENOENT" && options.allowMissing !== false) {
return { path: absolute, parent, exists: false, bytes: Buffer.alloc(0), digest: digest(Buffer.alloc(0)) };
}
throw error;
}
if (stat.isSymbolicLink() || !stat.isFile()) {
throw hostConfigError("AAS_ADAPTER_CONFIG_UNSAFE", "filesystem");
}
assertOwned(stat, options.expectedUid, absolute);
const handle = await fsp.open(absolute, "r");
let bytes;
let openedStat;
try {
openedStat = await handle.stat();
if (!openedStat.isFile() || openedStat.dev !== stat.dev || openedStat.ino !== stat.ino) {
throw hostConfigError("AAS_ADAPTER_CONFIG_CHANGED", "conflict");
}
bytes = await handle.readFile();
} finally {
await handle.close();
}
return {
path: absolute,
parent,
exists: true,
bytes,
digest: digest(bytes),
identity: {
dev: stat.dev,
ino: stat.ino,
uid: stat.uid,
gid: stat.gid,
mode: stat.mode & 0o7777,
size: stat.size,
},
};
}
function sameIdentity(left, right) {
if (!left || !right) return left === right;
return ["dev", "ino", "uid", "gid", "mode", "size"].every((key) => left[key] === right[key]);
}
async function fsyncDirectory(directoryPath) {
await fsyncDirectoryAsync(directoryPath);
}
async function writeExclusiveSynced(filePath, bytes, mode = 0o600) {
const handle = await fsp.open(filePath, "wx", mode);
try {
// On Windows the create mode does not constrain the inherited DACL. Make
// the still-empty file owner-only before any potentially sensitive bytes
// are written.
hardenWindowsPrivatePath(filePath, false);
await handle.writeFile(bytes);
await handle.chmod(mode);
await handle.sync();
} finally {
await handle.close();
}
}
module.exports = {
assertExplicitAbsolutePath,
assertOwned,
assertSafeDirectory,
assertWindowsPrivatePath,
copyWindowsAcl,
currentUid,
digest,
fsyncDirectory,
hardenWindowsPrivatePath,
inspectRegularFile,
sameIdentity,
writeExclusiveSynced,
};
@@ -0,0 +1,49 @@
"use strict";
const { hostConfigError } = require("./errors");
function assertString(value, field, maximum = 4096) {
if (typeof value !== "string" || value.length === 0 || value.length > maximum || value.includes("\0")) {
throw hostConfigError("AAS_ADAPTER_SERVER_INVALID", "invalidInput", { field });
}
return value;
}
function normalizeArgs(args) {
if (!Array.isArray(args) || args.length > 64) {
throw hostConfigError("AAS_ADAPTER_SERVER_INVALID", "invalidInput", { field: "args" });
}
return args.map((entry, index) => assertString(entry, `args[${index}]`));
}
function normalizeEnv(env) {
if (!env || typeof env !== "object" || Array.isArray(env) || Object.getPrototypeOf(env) !== Object.prototype) {
throw hostConfigError("AAS_ADAPTER_SERVER_INVALID", "invalidInput", { field: "env" });
}
const entries = Object.entries(env);
if (entries.length > 64) throw hostConfigError("AAS_ADAPTER_SERVER_INVALID", "invalidInput", { field: "env" });
return Object.fromEntries(entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, value]) => [
assertString(key, "env.key", 256),
typeof value === "string" && value.length <= 8192 && !value.includes("\0")
? value
: (() => { throw hostConfigError("AAS_ADAPTER_SERVER_INVALID", "invalidInput", { field: `env.${key}` }); })(),
]));
}
function normalizeServer(host, server) {
if (!server || typeof server !== "object" || Array.isArray(server)) {
throw hostConfigError("AAS_ADAPTER_SERVER_INVALID", "invalidInput");
}
if (host === "codex") {
if (server.enabled !== undefined && typeof server.enabled !== "boolean") {
throw hostConfigError("AAS_ADAPTER_SERVER_INVALID", "invalidInput", { field: "enabled" });
}
return { command: assertString(server.command, "command"), args: normalizeArgs(server.args), enabled: server.enabled ?? true };
}
if (host === "claude") {
return { command: assertString(server.command, "command"), args: normalizeArgs(server.args), env: normalizeEnv(server.env ?? {}) };
}
throw hostConfigError("AAS_ADAPTER_HOST_UNSUPPORTED", "invalidInput", { host });
}
module.exports = { normalizeServer };