diff --git a/README.md b/README.md index f104f851..1ebd521e 100644 --- a/README.md +++ b/README.md @@ -44,13 +44,14 @@ The open network feeds the premium layer. The premium layer funds the open netwo ## ๐Ÿ“ฆ What's inside (today) > **The registry is live and growing every week. All packages are free to download, fork and run.** +> The **In this repo** column is the open seed you can `git clone` today; the **Hosted registry** column is the full, continuously-evolved library on [superagentskill.com](https://superagentskill.com/marketplace). -| Type | Count | Examples | -|---|---|---| -| ๐Ÿ›  **Skills** | **370+** | code review, OWASP audit, SQL translator, OSINT, ECG triage, MEDDPICC discovery | -| ๐Ÿ“˜ **Playbooks** | **30+** | bug triage โ†’ fix โ†’ PR, content pipeline, incident response | -| ๐ŸŽญ **Souls** | **50+** | pragmatic SRE, empathetic support, senior product designer | -| ๐Ÿ›ก **Guardrails** | **50+** | no-PII, no-medical-advice, GDPR, brand safety | +| Type | In this repo (open seed) | Hosted registry | Examples | +|---|---|---|---| +| ๐Ÿ›  **Skills** | **76** | **370+** | code review, OWASP audit, SQL translator, OSINT, ECG triage, MEDDPICC discovery | +| ๐Ÿ“˜ **Playbooks** | **2** | **30+** | bug triage โ†’ fix โ†’ PR, content pipeline, incident response | +| ๐ŸŽญ **Souls** | **6** | **50+** | pragmatic SRE, empathetic support, senior product designer | +| ๐Ÿ›ก **Guardrails** | **2** | **50+** | no-PII, no-medical-advice, GDPR, brand safety | โžก๏ธ **[Browse the full marketplace at superagentskill.com โ†’](https://superagentskill.com/marketplace)** @@ -100,6 +101,7 @@ bun install && bun run dev - ๐Ÿ›ก **Proprietary adversarial harness.** Every skill is benchmarked against attacks before it can publish โ€” prompt injection, jailbreaks, exfiltration, blast-radius, policy bypass, PII/PHI leakage. Cases are curated per vertical (OWASP LLM, FINRA, HIPAA Safe Harbor, PCI-DSS, SRE). - ๐Ÿ” **Ed25519-signed releases.** Every published version is cryptographically signed. Verify offline with `npm run release:verify` โ€” exactly what air-gapped enterprise customers need. +- ๐Ÿงพ **Signed per-package trust attestations.** `npm run trust:attest` binds a package's exact content hash + version to its adversarial result and signs it with the release key. Your security team runs `npm run trust:verify -- --attestation pkg.trust.json --pkg pkg.yaml --pubkey pub.pem` and confirms offline that *this exact file* passed *N* adversarial cases โ€” no need to trust the hosted Trust Score endpoint. - ๐Ÿ“Š **Public, verifiable Trust Score.** A transparent weighted formula over adversarial robustness, real-world success rate, signed releases and package age. Embed the badge in your README; reviewers see the same number you do. - ๐Ÿ› **Verticalized Souls.** Fintech compliance officer (FINRA / Reg E / PCI-DSS), HIPAA-aware clinical liaison, SOC 2 auditor (TSC-mapped), Kubernetes SRE (blast-radius-first). Souls that cite the rule before the recommendation. - ๐Ÿ”Œ **One MCP endpoint, every IDE.** Claude, Cursor, ChatGPT, Continue, Cline โ€” same URL, same `npx super-agent install ` command. No SDK, no glue code. diff --git a/package.json b/package.json index a7ad08dc..a0a6eb60 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "preview": "vite preview", "lint": "eslint .", "format": "prettier --write .", - "validate:content": "node scripts/validate-content.mjs", + "validate:content": "node --experimental-strip-types scripts/validate-content.mjs", "sync:content": "node scripts/sync-content-to-registry.mjs", "sync:adversarial": "node scripts/sync-adversarial-cases.mjs", "eval:adversarial": "node scripts/eval-adversarial.mjs", @@ -19,8 +19,10 @@ "release:sign": "node scripts/sign-release-bundle.mjs", "release:verify": "node scripts/verify-release-bundle.mjs", "release:changelog": "node scripts/generate-changelog.mjs", + "trust:attest": "node scripts/generate-trust-attestation.mjs", + "trust:verify": "node scripts/verify-trust-attestation.mjs", "test": "npm run test:plain && npm run test:ts", - "test:plain": "node --test tests/adversarial-harness.test.mjs tests/trust.test.mjs tests/release-signing.test.mjs tests/cli-install.test.mjs", + "test:plain": "node --test tests/adversarial-harness.test.mjs tests/trust.test.mjs tests/release-signing.test.mjs tests/cli-install.test.mjs tests/trust-attestation.test.mjs", "test:ts": "node --experimental-strip-types --test tests/prompt-injection-guard.test.mjs tests/runtime.test.mjs tests/integrations.test.mjs tests/growth-revenue-split.test.mjs tests/trust-badge.test.mjs tests/bounties.test.mjs" }, "dependencies": { diff --git a/scripts/generate-trust-attestation.mjs b/scripts/generate-trust-attestation.mjs new file mode 100644 index 00000000..b64ca321 --- /dev/null +++ b/scripts/generate-trust-attestation.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +// Produces a signed, independently-verifiable trust attestation for a single +// package. It binds the package's exact content hash + version to the result +// of the adversarial harness, signed with the same Ed25519 release key +// (sign-release-bundle.mjs). A security team can hand the attestation + +// the package file to verify-trust-attestation.mjs and confirm offline that +// "this exact file passed N adversarial cases at score X" โ€” no need to trust +// the hosted Trust Score endpoint. +// +// Usage: +// node scripts/eval-adversarial.mjs --skill code-reviewer --mock --out eval.json +// SIGNING_PRIVATE_KEY=$(cat priv.pem) SIGNING_PUBLIC_KEY=$(cat pub.pem) \ +// node scripts/generate-trust-attestation.mjs \ +// --pkg content/skills/code-reviewer.yaml --eval eval.json + +import { createHash, createPrivateKey, createPublicKey, sign } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; +import { basename } from "node:path"; + +const args = parse(process.argv.slice(2)); +const pkgPath = args.pkg; +const evalPath = args.eval; +if (!pkgPath || !evalPath) { + console.error("Usage: --pkg --eval [--out ]"); + process.exit(1); +} + +const { SIGNING_PRIVATE_KEY, SIGNING_PUBLIC_KEY } = process.env; +if (!SIGNING_PRIVATE_KEY || !SIGNING_PUBLIC_KEY) { + console.error("SIGNING_PRIVATE_KEY and SIGNING_PUBLIC_KEY env vars required (PEM contents)."); + process.exit(1); +} +const priv = createPrivateKey(SIGNING_PRIVATE_KEY); +const pubDer = createPublicKey(SIGNING_PUBLIC_KEY).export({ format: "der", type: "spki" }); +const keyId = createHash("sha256").update(pubDer).digest("hex").slice(0, 16); + +const pkgBytes = readFileSync(pkgPath); +const pkgText = pkgBytes.toString("utf8"); +const contentSha256 = createHash("sha256").update(pkgBytes).digest("hex"); + +// Top-level scalar fields only โ€” enough to cross-check identity against the +// eval report. Avoids a YAML dependency so this stays in lockstep with the +// dependency-free offline verifier. +function topScalar(key) { + const m = pkgText.match(new RegExp(`^${key}:[ \\t]*["']?([^"'\\n#]+?)["']?[ \\t]*(?:#.*)?$`, "m")); + return m ? m[1].trim() : undefined; +} +const pkgSlug = topScalar("slug"); +const pkgVersion = topScalar("version"); +const pkgType = topScalar("type"); + +const report = JSON.parse(readFileSync(evalPath, "utf8")); +const rp = report.package ?? {}; +if (rp.slug && pkgSlug && rp.slug !== pkgSlug) { + console.error(`โœ— eval report is for slug "${rp.slug}" but package file is "${pkgSlug}"`); + process.exit(2); +} +if (rp.version && pkgVersion && rp.version !== pkgVersion) { + console.error(`โœ— eval report is for version "${rp.version}" but package file is "${pkgVersion}"`); + process.exit(2); +} + +const payload = { + schema_version: 1, + attested_at: new Date().toISOString(), + package: { + type: pkgType ?? rp.type ?? null, + slug: pkgSlug ?? rp.slug ?? null, + version: pkgVersion ?? rp.version ?? null, + file: basename(pkgPath), + }, + content_sha256: contentSha256, + adversarial: { + total: report.total ?? 0, + passed: report.passed ?? 0, + failed: report.failed ?? 0, + pass_rate: report.pass_rate ?? 0, + severity_weighted_score: report.severity_weighted_score ?? 0, + by_severity: report.by_severity ?? {}, + }, +}; + +const canonical = stableStringify(payload); +const payloadHash = createHash("sha256").update(canonical).digest("hex"); +const signature = sign(null, Buffer.from(payloadHash), priv).toString("base64"); + +const attestation = { ...payload, signing_key_id: keyId, algorithm: "ed25519", signature }; +const outPath = args.out ?? `${payload.package.slug ?? "package"}.trust.json`; +writeFileSync(outPath, `${JSON.stringify(attestation, null, 2)}\n`); + +console.log(`โœ“ trust attestation written: ${outPath}`); +console.log(` package: ${payload.package.type}:${payload.package.slug}@${payload.package.version}`); +console.log(` content: sha256:${contentSha256.slice(0, 16)}โ€ฆ`); +console.log(` adversarial: ${payload.adversarial.passed}/${payload.adversarial.total} cases, score ${(payload.adversarial.severity_weighted_score * 100).toFixed(1)}%`); +console.log(` signed by: key_id=${keyId}`); + +// Deterministic JSON: object keys sorted recursively so the signed bytes are +// reproducible regardless of insertion order. +function stableStringify(value) { + if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function parse(argv) { + const o = {}; + for (let i = 0; i < argv.length; i++) { + if (!argv[i].startsWith("--")) continue; + const k = argv[i].slice(2), n = argv[i + 1]; + if (!n || n.startsWith("--")) o[k] = true; + else { o[k] = n; i++; } + } + return o; +} diff --git a/scripts/validate-content.mjs b/scripts/validate-content.mjs index 6b2b3948..27d48978 100644 --- a/scripts/validate-content.mjs +++ b/scripts/validate-content.mjs @@ -9,6 +9,7 @@ import { readdirSync, readFileSync, statSync } from "node:fs"; import { join, basename } from "node:path"; import Ajv from "ajv"; import { parse as parseYaml } from "yaml"; +import { inspectContent } from "../src/lib/security/prompt-injection-guard.ts"; const ROOT = new URL("..", import.meta.url).pathname; const TYPES = ["skill", "playbook", "soul", "guardrail", "integration"]; @@ -26,6 +27,7 @@ const adversarialSchema = JSON.parse( const validateAdversarial = ajv.compile(adversarialSchema); let errors = 0; +let warnings = 0; const slugs = new Map(); // slug -> file const adversarialIds = new Map(); // id -> file @@ -64,6 +66,21 @@ for (const type of TYPES) { } else { slugs.set(pkg.slug, full); } + + // Prompt-injection gate for community contributions. The MCP upload path + // already scans (lib/uploads/uploads.server.ts); this closes the same hole + // for packages added via PR/git. `critical` signals (fake SYSTEM/control + // tokens, secret-exfiltration URLs) block the build; `high` is reported as + // a non-fatal warning since legitimate skill prose ("run this skill with + // โ€ฆ") trips the heuristic. Skills that legitimately demonstrate attacks + // (e.g. a prompt-injection tester) set `x_security_research: true` to opt + // out of the warning. + const guard = inspectContent(readFileSync(full, "utf8"), { fence: false }); + if (guard.severity === "critical") { + fail(full, `prompt-injection: critical signal(s) โ€” ${guard.findings.filter((g) => g.severity === "critical").map((g) => g.pattern).join("; ")}`); + } else if ((guard.severity === "high" || guard.severity === "medium") && pkg.x_security_research !== true) { + warn(full, `prompt-injection: ${guard.severity} signal(s) โ€” ${guard.findings.map((g) => g.pattern).join("; ")} (set x_security_research: true if intentional)`); + } } } @@ -114,8 +131,16 @@ function fail(file, msg) { console.error(`\u001b[31mโœ—\u001b[0m ${file.replace(ROOT, "")}: ${msg}`); } +function warn(file, msg) { + warnings++; + console.warn(`โš  ${file.replace(ROOT, "")}: ${msg}`); +} + if (errors > 0) { - console.error(`\n${errors} validation error(s).`); + console.error(`\n${errors} validation error(s)${warnings ? `, ${warnings} warning(s)` : ""}.`); process.exit(1); } -console.log(`โœ“ ${slugs.size} package(s) and ${adversarialIds.size} adversarial case(s) validated.`); +console.log( + `โœ“ ${slugs.size} package(s) and ${adversarialIds.size} adversarial case(s) validated.` + + (warnings ? ` ${warnings} warning(s)` : ""), +); diff --git a/scripts/verify-trust-attestation.mjs b/scripts/verify-trust-attestation.mjs new file mode 100644 index 00000000..968f7294 --- /dev/null +++ b/scripts/verify-trust-attestation.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node +// Offline verifier for a trust attestation produced by +// generate-trust-attestation.mjs. Designed to be run by a consumer's security +// team with nothing but: the package file, the .trust.json, and the publisher's +// SIGNING_PUBLIC_KEY.pem. No network, no trust in the hosted registry. +// +// Usage: +// node scripts/verify-trust-attestation.mjs \ +// --attestation code-reviewer.trust.json \ +// --pkg content/skills/code-reviewer.yaml \ +// --pubkey pub.pem +// +// Exits non-zero if the package was tampered with or the signature is invalid. + +import { createHash, createPublicKey, verify } from "node:crypto"; +import { readFileSync } from "node:fs"; + +const args = parse(process.argv.slice(2)); +if (!args.attestation || !args.pkg) { + console.error("Usage: --attestation --pkg [--pubkey ]"); + process.exit(1); +} + +const att = JSON.parse(readFileSync(args.attestation, "utf8")); +const pubPem = args.pubkey + ? readFileSync(args.pubkey, "utf8") + : process.env.SIGNING_PUBLIC_KEY; +if (!pubPem) { + console.error("Provide --pubkey or SIGNING_PUBLIC_KEY env (PEM contents)."); + process.exit(1); +} +const pubKey = createPublicKey(pubPem); +const pubDer = pubKey.export({ format: "der", type: "spki" }); +const fingerprint = createHash("sha256").update(pubDer).digest("hex").slice(0, 16); + +let failed = 0; + +if (fingerprint !== att.signing_key_id) { + console.error(`โœ— public key fingerprint mismatch: attestation=${att.signing_key_id} key=${fingerprint}`); + failed++; +} + +const pkgBytes = readFileSync(args.pkg); +const contentSha256 = createHash("sha256").update(pkgBytes).digest("hex"); +if (contentSha256 !== att.content_sha256) { + console.error(`โœ— package content changed since attestation`); + console.error(` attested: sha256:${att.content_sha256}`); + console.error(` actual: sha256:${contentSha256}`); + failed++; +} + +const { signature, signing_key_id: _kid, algorithm: _alg, ...payload } = att; +const payloadHash = createHash("sha256").update(stableStringify(payload)).digest("hex"); +const sigOk = signature + ? verify(null, Buffer.from(payloadHash), pubKey, Buffer.from(signature, "base64")) + : false; +if (!sigOk) { + console.error(`โœ— invalid signature โ€” attestation body does not match the signature`); + failed++; +} + +if (failed > 0) { + console.error(`\n${failed} verification failure(s). DO NOT trust this package.`); + process.exit(3); +} + +const a = att.adversarial ?? {}; +console.log(`โœ“ trust attestation VERIFIED`); +console.log(` package: ${att.package?.type}:${att.package?.slug}@${att.package?.version}`); +console.log(` content: sha256:${contentSha256} (matches)`); +console.log(` attested at: ${att.attested_at}`); +console.log(` adversarial: ${a.passed}/${a.total} cases passed, severity-weighted ${(((a.severity_weighted_score) ?? 0) * 100).toFixed(1)}%`); +console.log(` signed by: key_id=${fingerprint}`); + +function stableStringify(value) { + if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function parse(argv) { + const o = {}; + for (let i = 0; i < argv.length; i++) { + if (!argv[i].startsWith("--")) continue; + const k = argv[i].slice(2), n = argv[i + 1]; + if (!n || n.startsWith("--")) o[k] = true; + else { o[k] = n; i++; } + } + return o; +} diff --git a/tests/trust-attestation.test.mjs b/tests/trust-attestation.test.mjs new file mode 100644 index 00000000..49ce2ebc --- /dev/null +++ b/tests/trust-attestation.test.mjs @@ -0,0 +1,80 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { generateKeyPairSync } from "node:crypto"; +import { mkdtempSync, writeFileSync, appendFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ROOT = new URL("..", import.meta.url).pathname; + +function keys() { + const { publicKey, privateKey } = generateKeyPairSync("ed25519"); + return { + pub: publicKey.export({ format: "pem", type: "spki" }).toString(), + priv: privateKey.export({ format: "pem", type: "pkcs8" }).toString(), + }; +} + +function fixture() { + const dir = mkdtempSync(join(tmpdir(), "sas-trust-")); + const pkg = join(dir, "demo-skill.yaml"); + writeFileSync(pkg, "slug: demo-skill\nname: Demo\ntype: skill\nversion: 1.0.0\n"); + const ev = join(dir, "eval.json"); + writeFileSync( + ev, + JSON.stringify({ + package: { type: "skill", slug: "demo-skill", version: "1.0.0" }, + total: 10, passed: 9, failed: 1, pass_rate: 0.9, + severity_weighted_score: 0.95, by_severity: { high: { total: 4, passed: 4, pass_rate: 1 } }, + }), + ); + return { dir, pkg, ev, att: join(dir, "demo-skill.trust.json") }; +} + +function attest(pkg, ev, att, k) { + return spawnSync( + "node", + ["scripts/generate-trust-attestation.mjs", "--pkg", pkg, "--eval", ev, "--out", att], + { cwd: ROOT, encoding: "utf8", env: { ...process.env, SIGNING_PRIVATE_KEY: k.priv, SIGNING_PUBLIC_KEY: k.pub } }, + ); +} + +function doVerify(att, pkg, pubPem) { + return spawnSync( + "node", + ["scripts/verify-trust-attestation.mjs", "--attestation", att, "--pkg", pkg], + { cwd: ROOT, encoding: "utf8", env: { ...process.env, SIGNING_PUBLIC_KEY: pubPem } }, + ); +} + +test("attest + verify round-trips", () => { + const { pkg, ev, att } = fixture(); + const k = keys(); + const a = attest(pkg, ev, att, k); + assert.equal(a.status, 0, a.stderr); + const v = doVerify(att, pkg, k.pub); + assert.equal(v.status, 0, v.stderr); + assert.match(v.stdout, /VERIFIED/); + assert.match(v.stdout, /9\/10 cases passed/); +}); + +test("tampered package fails verification", () => { + const { pkg, ev, att } = fixture(); + const k = keys(); + attest(pkg, ev, att, k); + appendFileSync(pkg, "\n# malicious edit\n"); + const v = doVerify(att, pkg, k.pub); + assert.notEqual(v.status, 0); + assert.match(v.stderr, /content changed/); +}); + +test("wrong public key is rejected by fingerprint check", () => { + const { pkg, ev, att } = fixture(); + const k = keys(); + const k2 = keys(); + attest(pkg, ev, att, k); + const v = doVerify(att, pkg, k2.pub); + assert.notEqual(v.status, 0); + assert.match(v.stderr, /fingerprint mismatch/); +});