@sellable/install
Version:
One-command installer for Sellable MCP in Claude Code, Codex, and Hermes
393 lines (376 loc) • 12 kB
JavaScript
import { createHash } from "node:crypto";
import {
chmodSync,
existsSync,
lstatSync,
mkdirSync,
readFileSync,
readdirSync,
writeFileSync,
} from "node:fs";
import { dirname, isAbsolute, join, posix, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { containsSoulSecretMaterial } from "./soul-artifact-validator.mjs";
const HERE = dirname(fileURLToPath(import.meta.url));
const FRAGMENTS = join(HERE, "default-profile-bundles");
const SCHEMA = "sellable-agent-default-profile-fragment/v1";
const BUNDLE_SCHEMA = "sellable-agent-default-profile-bundle/v1";
const ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
const SKILL_PATH =
/^skills\/sellable-defaults\/([a-z0-9][a-z0-9-]{0,63})(?:\/.*)?$/;
const PUBLIC_SKILL_PATH =
/^skills\/sellable\/([a-z0-9][a-z0-9-]{0,127})\/SKILL\.md$/;
const CRON_PATH = /^crons\/([a-z0-9][a-z0-9-]{0,63})\.json$/;
const MAX_FILES = 64;
const MAX_FILE_BYTES = 64 * 1024;
// A fragment embeds the exact bytes of each declared file so the closed-root
// readback can reject drift. Five shared skills currently total about 129 KiB;
// keep bounded headroom here while the compiled bundle remains capped at 1 MiB.
const MAX_FRAGMENT_BYTES = 192 * 1024;
const MAX_TOTAL_BYTES = 1024 * 1024;
function readNativeSkillName(content) {
const lines = content.split("\n");
if (lines[0] !== "---") reject("skill_frontmatter_rejected");
const end = lines.indexOf("---", 1);
if (end < 2) reject("skill_frontmatter_rejected");
const names = lines
.slice(1, end)
.map((line) => /^name:\s*([a-z0-9][a-z0-9-]{0,63})\s*$/.exec(line)?.[1])
.filter(Boolean);
if (names.length !== 1 || !ID.test(names[0])) {
reject("skill_frontmatter_rejected");
}
return names[0];
}
function sha256(value) {
return createHash("sha256").update(value).digest("hex");
}
function canonicalJson(value) {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (value && typeof value === "object") {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
function reject(code) {
throw new Error(`default_profile_bundle_${code}`);
}
const SHARED_FRAGMENT_AUXILIARY_FILES = new Set([
"scripts/customer-program-due.py",
]);
function assertClosedFragmentRoot(path, fragment) {
const root = dirname(path);
const declared = new Map(
fragment.files.map((file) => [file.path, file.content])
);
const visit = (candidate, relativePath = "") => {
const entry = lstatSync(candidate);
if (entry.isSymbolicLink()) reject("symlink_rejected");
if (entry.isDirectory()) {
for (const name of readdirSync(candidate).sort()) {
visit(
join(candidate, name),
relativePath ? `${relativePath}/${name}` : name
);
}
return;
}
if (!entry.isFile()) reject("file_type_rejected");
if (relativePath === "manifest.fragment.json") return;
if (
fragment.fragmentId === "shared" &&
SHARED_FRAGMENT_AUXILIARY_FILES.has(relativePath)
) {
if (
(entry.mode & 0o111) === 0 ||
entry.size < 1 ||
entry.size > MAX_FILE_BYTES
) {
reject("auxiliary_file_rejected");
}
return;
}
const expected = declared.get(relativePath);
if (typeof expected !== "string") reject("undeclared_file_rejected");
if (!readFileSync(candidate).equals(Buffer.from(expected, "utf8"))) {
reject("declared_file_mismatch");
}
};
visit(root);
}
function readFragment(path) {
if (!isAbsolute(path) || !existsSync(path)) reject("fragment_missing");
const manifestStat = lstatSync(path);
if (manifestStat.isSymbolicLink() || !manifestStat.isFile()) {
reject("fragment_invalid");
}
let value;
try {
const bytes = readFileSync(path);
if (bytes.byteLength > MAX_FRAGMENT_BYTES) reject("fragment_too_large");
value = JSON.parse(bytes.toString("utf8"));
} catch (error) {
if (
error instanceof Error &&
error.message.startsWith("default_profile_bundle_")
) {
throw error;
}
reject("fragment_invalid");
}
if (
!value ||
typeof value !== "object" ||
Array.isArray(value) ||
Object.keys(value).sort().join("\0") !==
[
"bundleVersion",
"files",
"fragmentId",
"kind",
"materializerReceipts",
"schemaVersion",
]
.sort()
.join("\0") ||
value.schemaVersion !== SCHEMA ||
!ID.test(value.fragmentId ?? "") ||
!["SHARED", "CUSTOMER", "ADMIN", "FIXTURE"].includes(value.kind) ||
!Number.isSafeInteger(value.bundleVersion) ||
value.bundleVersion < 1 ||
value.bundleVersion > 2_147_483_647 ||
!Array.isArray(value.materializerReceipts) ||
!Array.isArray(value.files)
) {
reject("fragment_invalid");
}
assertClosedFragmentRoot(path, value);
return value;
}
function validatePath(path) {
if (
typeof path !== "string" ||
path.length < 1 ||
path.length > 256 ||
isAbsolute(path) ||
path.includes("\\") ||
path.includes("\0") ||
posix.normalize(path) !== path ||
path.startsWith("../")
) {
reject("path_rejected");
}
}
function normalizeFile(value, fragmentKind) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
reject("file_invalid");
}
const allowed =
value.type === "cron"
? ["content", "id", "mode", "name", "ownership", "path", "type"]
: ["content", "id", "mode", "ownership", "path", "type"];
if (Object.keys(value).sort().join("\0") !== allowed.sort().join("\0")) {
reject("file_invalid");
}
validatePath(value.path);
if (
!ID.test(value.id ?? "") ||
!["skill", "cron"].includes(value.type) ||
value.ownership !== "system_managed" ||
value.mode !== "0444" ||
typeof value.content !== "string"
) {
reject("file_invalid");
}
const bytes = Buffer.from(value.content, "utf8");
if (
bytes.byteLength < 1 ||
bytes.byteLength > MAX_FILE_BYTES ||
containsSoulSecretMaterial(bytes)
) {
reject("content_rejected");
}
let nativeName = null;
if (value.type === "skill") {
nativeName = readNativeSkillName(value.content);
const managedMatch = SKILL_PATH.exec(value.path);
const publicMatch = PUBLIC_SKILL_PATH.exec(value.path);
const managedPathValid =
managedMatch?.[1] === value.id && value.path.endsWith("/SKILL.md");
const canonicalPublicPathValid =
publicMatch?.[1] === nativeName && nativeName === `sellable-${value.id}`;
if (!managedPathValid && !canonicalPublicPathValid) {
reject("skill_path_rejected");
}
} else {
const match = CRON_PATH.exec(value.path);
if (
!match ||
value.name !== match[1] ||
!value.name.startsWith("sellable-default-")
) {
reject("cron_path_rejected");
}
}
return Object.freeze({
id: value.id,
type: value.type,
ownership: value.ownership,
kind: fragmentKind,
path: value.path,
mode: value.mode,
...(value.type === "cron" ? { name: value.name } : { nativeName }),
content: value.content,
sha256: sha256(bytes),
});
}
function writeBundle(outputRoot, result) {
mkdirSync(outputRoot, { recursive: true, mode: 0o755 });
for (const file of result.files) {
const target = join(outputRoot, ...file.path.split("/"));
mkdirSync(dirname(target), { recursive: true, mode: 0o755 });
writeFileSync(target, file.content, { mode: 0o444 });
chmodSync(target, 0o444);
}
writeFileSync(
join(outputRoot, "manifest.json"),
`${canonicalJson({ ...result.manifest, bundleDigest: result.bundleDigest })}\n`,
{ mode: 0o444 }
);
chmodSync(join(outputRoot, "manifest.json"), 0o444);
}
/** Build the one canonical kind bundle. No identity-specific digest is imported. */
export function buildDefaultProfileBundle({
kind,
fragmentPaths,
adminFragmentPath,
fixtureVersion,
outputRoot,
} = {}) {
if (!["CUSTOMER", "ADMIN"].includes(kind)) reject("kind_rejected");
if (fixtureVersion !== undefined && ![1, 2].includes(fixtureVersion)) {
reject("fixture_rejected");
}
const paths = fragmentPaths
? [...fragmentPaths]
: [
join(FRAGMENTS, "shared", "manifest.fragment.json"),
kind === "CUSTOMER"
? join(FRAGMENTS, "customer", "manifest.fragment.json")
: resolve(adminFragmentPath ?? ""),
...(fixtureVersion
? [
join(
FRAGMENTS,
"fixtures",
`v${fixtureVersion}`,
"manifest.fragment.json"
),
]
: []),
];
if (!Array.isArray(paths) || paths.length < 1 || paths.length > 4) {
reject("fragment_inventory_rejected");
}
const fragments = paths.map((path) => readFragment(resolve(path)));
if (
new Set(fragments.map((fragment) => fragment.fragmentId)).size !==
fragments.length
) {
reject("fragment_collision");
}
for (const fragment of fragments) {
if (
!["SHARED", kind].includes(fragment.kind) &&
!(fixtureVersion && fragment.kind === "FIXTURE")
) {
reject("kind_isolation_rejected");
}
}
const files = fragments
.flatMap((fragment) =>
fragment.files.map((file) => normalizeFile(file, fragment.kind))
)
.sort((left, right) => left.path.localeCompare(right.path));
if (
files.length > MAX_FILES ||
files.reduce((sum, file) => sum + Buffer.byteLength(file.content), 0) >
MAX_TOTAL_BYTES ||
new Set(files.map((file) => file.path)).size !== files.length ||
new Set(
files.filter((file) => file.type === "skill").map((file) => file.id)
).size !== files.filter((file) => file.type === "skill").length ||
new Set(
files
.filter((file) => file.type === "skill")
.map((file) => file.nativeName)
).size !== files.filter((file) => file.type === "skill").length ||
new Set(
files.filter((file) => file.type === "cron").map((file) => file.name)
).size !== files.filter((file) => file.type === "cron").length
) {
reject("entry_collision");
}
const materializerReceipts = [
...new Set(fragments.flatMap((fragment) => fragment.materializerReceipts)),
].sort();
if (
materializerReceipts.some(
(path) =>
path !== "SOUL.md" &&
path !== "skills/sellable/SKILL.md" &&
path !== "skills/sellable-admin/SKILL.md"
)
) {
reject("materializer_receipt_rejected");
}
const bundleVersion = Math.max(
...fragments.map((fragment) => fragment.bundleVersion)
);
const inventory = files.map(({ content: _content, ...file }) => file);
const manifest = Object.freeze({
schemaVersion: BUNDLE_SCHEMA,
kind,
bundleVersion,
materializerReceipts,
files: inventory,
});
const result = Object.freeze({
bundleVersion,
manifest,
files,
bundleDigest: sha256(canonicalJson({ manifest, files })),
});
if (outputRoot !== undefined) {
if (!isAbsolute(outputRoot)) reject("output_root_rejected");
writeBundle(outputRoot, result);
}
return result;
}
function argumentValue(flag) {
const index = process.argv.indexOf(flag);
return index >= 0 ? process.argv[index + 1] : undefined;
}
if (
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
const result = buildDefaultProfileBundle({
kind: argumentValue("--kind"),
adminFragmentPath: argumentValue("--admin-fragment"),
fixtureVersion: argumentValue("--fixture-version")
? Number(argumentValue("--fixture-version"))
: undefined,
outputRoot: resolve(argumentValue("--output") ?? ""),
});
process.stdout.write(
`${JSON.stringify({
bundleVersion: result.bundleVersion,
bundleDigest: result.bundleDigest,
})}\n`
);
}