Files
clawsec/scripts/ci/generate_skill_release_trust_packet.mjs
T
davida-ps 59d54ed778 fix(skills): namespace ClawHub skill slugs (#263)
* fix(release): map ClawHub publish slugs

* fix(release): share skill platform parsing
2026-06-10 16:39:19 +03:00

360 lines
11 KiB
JavaScript

#!/usr/bin/env node
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { installAgentForSkill, PLATFORM_KEYS } from "./skill_platforms.mjs";
const KNOWN_AGENT_TYPES = new Set(["codex", "hermes-agent", "openclaw", "universal"]);
function usage() {
return [
"Usage: node scripts/ci/generate_skill_release_trust_packet.mjs <skill-dir> <output-dir> [options]",
"",
"Options:",
" --repository <owner/repo> Source repository used in install instructions",
" --tag <tag> Release tag for this skill",
" --source-ref <ref> Source ref for npx skills examples",
].join("\n");
}
function parseArgs(argv) {
const positional = [];
const options = {
repository: "prompt-security/clawsec",
tag: "",
sourceRef: "main",
};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--repository") {
options.repository = argv[++i];
} else if (token === "--tag") {
options.tag = argv[++i];
} else if (token === "--source-ref") {
options.sourceRef = argv[++i];
} else if (token === "--help" || token === "-h") {
console.log(usage());
process.exit(0);
} else if (token.startsWith("--")) {
throw new Error(`Unknown option: ${token}`);
} else {
positional.push(token);
}
}
if (positional.length !== 2) {
throw new Error(usage());
}
return {
skillDir: positional[0],
outputDir: positional[1],
...options,
};
}
function parseFrontmatter(markdown) {
if (!markdown.startsWith("---\n")) {
return {};
}
const end = markdown.indexOf("\n---", 4);
if (end === -1) {
return {};
}
const result = {};
const frontmatter = markdown.slice(4, end).split("\n");
for (const line of frontmatter) {
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
if (match) {
result[match[1]] = match[2].replace(/^["']|["']$/g, "").trim();
}
}
return result;
}
function asArray(value) {
if (Array.isArray(value)) {
return value.filter((item) => item !== null && item !== undefined).map(String);
}
if (typeof value === "string" && value.trim()) {
return [value.trim()];
}
return [];
}
function unique(values) {
return [...new Set(values.filter(Boolean))];
}
function detectPlatform(skill) {
for (const key of PLATFORM_KEYS) {
if (skill[key] && typeof skill[key] === "object") {
return key;
}
}
return skill.platform || "agent-skills";
}
function platformMetadata(skill, platform) {
const direct = skill[platform];
return direct && typeof direct === "object" ? direct : {};
}
function collectRequiredBinaries(metadata) {
const requires = metadata.requires && typeof metadata.requires === "object" ? metadata.requires : {};
const bins = asArray(requires.bins);
for (const [key, value] of Object.entries(requires)) {
if (key !== "bins" && typeof value === "string") {
bins.push(key);
}
}
return unique(bins);
}
function collectOptionalBinaries(metadata) {
return unique([
...asArray(metadata.runtime?.optional_bins),
...asArray(metadata.runtime?.optionalBins),
]);
}
function collectRequiredEnv(metadata) {
const requires = metadata.requires && typeof metadata.requires === "object" ? metadata.requires : {};
return unique([
...asArray(requires.env),
...asArray(metadata.runtime?.required_env),
...asArray(metadata.runtime?.requiredEnv),
]);
}
function collectOptionalEnv(metadata) {
return unique([
...asArray(metadata.runtime?.optional_env),
...asArray(metadata.runtime?.optionalEnv),
]);
}
function stringifyCapabilities(skill, metadata) {
const capabilities = metadata.capabilities ?? skill.capabilities ?? {};
if (Array.isArray(capabilities)) {
return capabilities;
}
if (capabilities && typeof capabilities === "object") {
return Object.entries(capabilities).map(([key, value]) => `${key}: ${String(value)}`);
}
if (typeof capabilities === "string") {
return [capabilities];
}
return [];
}
function requireField(skill, fieldName) {
if (!skill[fieldName] || typeof skill[fieldName] !== "string" || !skill[fieldName].trim()) {
throw new Error(`skill.json missing required trust-packet field: ${fieldName}`);
}
return skill[fieldName].trim();
}
function codeBlock(command) {
return ["```bash", command, "```"].join("\n");
}
function buildPermissions({ skill, metadata, platform, generatedAt }) {
const execution = metadata.execution && typeof metadata.execution === "object" ? metadata.execution : {};
const permissions = {
schema_version: "1",
generated_at: generatedAt,
skill: skill.name,
version: skill.version,
platform,
required_binaries: collectRequiredBinaries(metadata),
optional_binaries: collectOptionalBinaries(metadata),
required_env: collectRequiredEnv(metadata),
optional_env: collectOptionalEnv(metadata),
network_egress: execution.network_egress || "Not declared in skill metadata.",
persistence: execution.persistence || "Not declared in skill metadata.",
automatic_execution: typeof execution.always === "boolean" ? execution.always : "Not declared in skill metadata.",
capabilities: stringifyCapabilities(skill, metadata),
operator_review: asArray(metadata.operator_review),
};
return permissions;
}
function buildSkillCard({ skill, frontmatter, permissions, repository, tag, sourceRef }) {
const homepage = skill.homepage || frontmatter.homepage || `https://github.com/${repository}`;
const supportRef = `${repository}@${tag || sourceRef}`;
const licenseRef = `https://github.com/${repository}/blob/${tag || sourceRef}/LICENSE`;
const outputTypes = ["Markdown instructions", "release artifact files"];
if (permissions.capabilities.length > 0) {
outputTypes.push("local security findings or status reports");
}
return `# Skill Card
## Description
The \`${skill.name}\` skill provides this capability: ${skill.description}
This skill is intended for operator-reviewed security workflows, not unattended production mutation without the review steps declared in the skill instructions.
## Owner
prompt-security
## License/Terms of Use
${skill.license}
License reference: ${licenseRef}
Project homepage: ${homepage}
## Use Case
Use this skill for ${permissions.platform} workflows where an agent or operator needs the capability described in \`${skill.name}\`.
## Deployment Geography for Use
Global, subject to the operator's local compliance, network, and data-handling requirements.
## Known Risks and Mitigations
Risk: The skill may run commands, inspect local files, install hooks, or fetch remote security metadata depending on the workflow.
Mitigation: Review \`permissions.json\`, \`SKILL.md\`, and the signed \`checksums.json\` before enabling the skill. Keep high-impact actions approval-gated.
Risk: Security findings and remediation guidance can be incomplete or wrong.
Mitigation: Treat output as operator guidance. Review proposed removals, installs, configuration changes, and reports before acting.
## References
- Source release: ${supportRef}
- Skill instructions: SKILL.md
- Permission summary: permissions.json
- SkillSpector scan: skillspector-report.md
- Signed release manifest: checksums.json and checksums.sig
## Skill Output
Output type(s): ${outputTypes.join(", ")}
Output format: Markdown, JSON, shell commands, or local files as documented by the skill.
Output parameters: See \`SKILL.md\`, \`permissions.json\`, and release checksums for exact files and side effects.
Other properties: Release assets are covered by signed SHA-256 checksums.
## Skill Version
${skill.version}${tag ? ` (${tag})` : ""}
## Ethical Considerations
Use this skill only on systems, agents, repositories, and workspaces where you have authorization. Review generated security reports before sharing them because they may contain operational details.
`;
}
function buildInstallDoc({ skill, repository, tag, sourceRef }) {
const refSuffix = sourceRef && sourceRef !== "main" ? `#${sourceRef}` : "";
const source = `${repository}${refSuffix}`;
const releaseUrl = tag ? `https://github.com/${repository}/releases/tag/${tag}` : `https://github.com/${repository}`;
const agent = installAgentForSkill(skill, KNOWN_AGENT_TYPES);
return `# Install and Update ${skill.name}
## Install With Agent Skills CLI
Harness-aware global install:
${codeBlock(`npx skills add ${source} --skill ${skill.name} --agent ${agent} --global --yes`)}
Project-local install for compatible agents:
${codeBlock(`npx skills add ${source} --skill ${skill.name} --yes`)}
## Update
Update this skill when installed through the Skills CLI:
${codeBlock(`npx skills update ${skill.name}`)}
List installed skills:
${codeBlock("npx skills list")}
## Verify Release Artifact
When installing from a GitHub release instead of the Skills CLI, download the archive, \`checksums.json\`, \`checksums.sig\`, and \`signing-public.pem\` from:
${releaseUrl}
Verify \`checksums.json\` before trusting the archive or standalone files.
`;
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const skillDir = path.resolve(args.skillDir);
const outputDir = path.resolve(args.outputDir);
const skillJsonPath = path.join(skillDir, "skill.json");
const skillMdPath = path.join(skillDir, "SKILL.md");
const [skillJsonRaw, skillMdRaw] = await Promise.all([
readFile(skillJsonPath, "utf8"),
readFile(skillMdPath, "utf8"),
]);
const skill = JSON.parse(skillJsonRaw);
const frontmatter = parseFrontmatter(skillMdRaw);
skill.name = requireField(skill, "name");
skill.version = requireField(skill, "version");
skill.description = requireField(skill, "description");
skill.license = requireField(skill, "license");
const platform = detectPlatform(skill);
const metadata = platformMetadata(skill, platform);
const generatedAt = new Date().toISOString();
const permissions = buildPermissions({ skill, metadata, platform, generatedAt });
await mkdir(outputDir, { recursive: true });
await Promise.all([
writeFile(
path.join(outputDir, "permissions.json"),
`${JSON.stringify(permissions, null, 2)}\n`,
),
writeFile(
path.join(outputDir, "skill-card.md"),
buildSkillCard({
skill,
frontmatter,
permissions,
repository: args.repository,
tag: args.tag,
sourceRef: args.sourceRef,
}),
),
writeFile(
path.join(outputDir, "install.md"),
buildInstallDoc({
skill,
repository: args.repository,
tag: args.tag,
sourceRef: args.sourceRef,
}),
),
]);
console.log(`Generated release trust packet for ${skill.name} in ${outputDir}`);
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});