From fc49b0eac8f96b7f05db16d17306102cd6b12bbf Mon Sep 17 00:00:00 2001 From: gitadmin Date: Sat, 2 May 2026 13:07:38 +0000 Subject: [PATCH] feat: add routes/skills.ts --- routes/skills.ts | 869 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 869 insertions(+) create mode 100644 routes/skills.ts diff --git a/routes/skills.ts b/routes/skills.ts new file mode 100644 index 0000000..f62da40 --- /dev/null +++ b/routes/skills.ts @@ -0,0 +1,869 @@ +/** + * skills.agentify.help — VCAP Skills Repository + * + * A covenant-governed registry for agent skills and expert capabilities. + * Every skill entry carries a SHA-256 content hash and VCAP attestation. + * The hash is the integrity guarantee: any hidden line, hidden font, or + * injected instruction breaks the hash and is detectable by anyone. + * + * Domain: skills.agentify.help (Bunny DNS CNAME → agentify.help → Fly.io) + * Published under WellSpr.ing covenant. April 2026. + */ + +import express from "express"; +import type { Express, Request, Response } from "express"; +import { createHash, createHmac } from "crypto"; +import { pool } from "../db"; + +const SIGNING_KEY = process.env.ODY_SIGNING_KEY_B64 + ? Buffer.from(process.env.ODY_SIGNING_KEY_B64, "base64").toString("hex").slice(0, 64) + : "skills-agentify-help-dev-key-placeholder-not-for-production"; + +// ── DB setup ────────────────────────────────────────────────────────────────── +async function ensureSkillsTable() { + await pool.query(` + CREATE TABLE IF NOT EXISTS vcap_skill_registry ( + id SERIAL PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + summary TEXT NOT NULL DEFAULT '', + description TEXT NOT NULL DEFAULT '', + skill_type TEXT NOT NULL DEFAULT 'agent_skill', + author_name TEXT NOT NULL, + author_slug TEXT NOT NULL DEFAULT '', + content TEXT NOT NULL DEFAULT '', + content_hash TEXT NOT NULL DEFAULT '', + vcap_scope TEXT NOT NULL DEFAULT 'skills:general:read:public', + attestation_id TEXT NOT NULL DEFAULT '', + signature TEXT NOT NULL DEFAULT '', + tags TEXT[] NOT NULL DEFAULT '{}', + version TEXT NOT NULL DEFAULT '1.0.0', + covenant_url TEXT NOT NULL DEFAULT 'https://wellspr.ing/constitution', + status TEXT NOT NULL DEFAULT 'active', + registered_via TEXT NOT NULL DEFAULT 'web', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ) + `); + console.log("[skills.agentify.help] vcap_skill_registry table ready"); +} + +// ── Slugify ─────────────────────────────────────────────────────────────────── +function toSlug(s: string): string { + return s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").trim(); +} + +// ── Hash + attestation ──────────────────────────────────────────────────────── +function sha256(content: string): string { + return "sha256:" + createHash("sha256").update(content, "utf8").digest("hex"); +} + +function signAttestation(slug: string, contentHash: string, authorSlug: string, publishedAt: string): string { + const payload = [slug, contentHash, authorSlug, publishedAt].join("|"); + return createHmac("sha256", SIGNING_KEY).update(payload).digest("hex"); +} + +function buildAttestation(row: any) { + return { + vcap: "1.0", + skill_slug: row.slug, + skill_name: row.name, + skill_type: row.skill_type, + author_name: row.author_name, + author_slug: row.author_slug, + version: row.version, + vcap_scope: row.vcap_scope, + content_hash: row.content_hash, + content_length: (row.content || "").length, + covenant: row.covenant_url, + published_at: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at), + attestation_id: row.attestation_id, + signed_by: "skills.agentify.help", + signature: row.signature, + verify_url: `https://skills.agentify.help/skills/${row.slug}/attest.json`, + raw_url: `https://skills.agentify.help/skills/${row.slug}/raw`, + }; +} + +// ── Rate limiter ────────────────────────────────────────────────────────────── +const _rl = new Map(); +function rateCheck(key: string, max: number, windowMs: number): boolean { + const now = Date.now(); + const e = _rl.get(key); + if (!e || now > e.t) { _rl.set(key, { n: 1, t: now + windowMs }); return true; } + if (e.n >= max) return false; + e.n++; + return true; +} +setInterval(() => { const now = Date.now(); for (const [k, v] of _rl) if (now > v.t) _rl.delete(k); }, 600_000).unref(); + +// ── MCP tools ───────────────────────────────────────────────────────────────── +const SK_MCP_TOOLS = [ + { + name: "discover_skills", + description: "Browse and search the VCAP skills repository. Filter by type, author, or tags. Returns skill summaries with content hashes.", + inputSchema: { + type: "object", + properties: { + q: { type: "string", description: "Search query (name, author, tags)" }, + skill_type: { type: "string", description: "Filter by type: agent_skill, persona_skill, domain_skill, craft_skill, civic_skill" }, + author_slug: { type: "string", description: "Filter by author slug" }, + limit: { type: "number", description: "Max results (default 20)" } + } + } + }, + { + name: "get_skill", + description: "Fetch a complete skill by slug. Returns full content, attestation, and SHA-256 hash for integrity verification.", + inputSchema: { + type: "object", + required: ["slug"], + properties: { + slug: { type: "string", description: "Skill slug" } + } + } + }, + { + name: "submit_skill", + description: "Publish a new skill to the registry. The content is hashed on receipt and a VCAP attestation is generated. Returns the attestation.", + inputSchema: { + type: "object", + required: ["name", "author_name", "content"], + properties: { + name: { type: "string", description: "Skill name" }, + summary: { type: "string", description: "One-sentence summary" }, + description: { type: "string", description: "Full description" }, + skill_type: { type: "string", description: "Type: agent_skill | persona_skill | domain_skill | craft_skill | civic_skill" }, + author_name: { type: "string", description: "Author's full name" }, + content: { type: "string", description: "The skill file content (plain text / Markdown)" }, + vcap_scope: { type: "string", description: "SGS scope string (e.g. skills:herbalism:read:public)" }, + tags: { type: "array", items: { type: "string" }, description: "Array of tags" }, + version: { type: "string", description: "Version string (default 1.0.0)" } + } + } + }, + { + name: "verify_integrity", + description: "Verify a skill file's integrity against its published attestation. Provide the content you hold — returns whether it matches the registry hash. Detects any tampering, hidden text, or injection.", + inputSchema: { + type: "object", + required: ["slug", "content"], + properties: { + slug: { type: "string", description: "Skill slug to verify against" }, + content: { type: "string", description: "The skill file content you are holding" } + } + } + } +]; + +// ── MCP tool handler ────────────────────────────────────────────────────────── +async function handleSkillTool(name: string, args: any) { + const text = (obj: any) => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] }); + + if (name === "discover_skills") { + const limit = Math.min(Number(args.limit) || 20, 50); + const { rows } = await pool.query(` + SELECT id AS registration_number, slug, name, summary, skill_type, author_name, author_slug, + content_hash, vcap_scope, tags, version, created_at + FROM vcap_skill_registry + WHERE status = 'active' + AND ($1::text IS NULL OR name ILIKE '%' || $1 || '%' OR author_name ILIKE '%' || $1 || '%' OR $1 = ANY(tags)) + AND ($2::text IS NULL OR skill_type = $2) + AND ($3::text IS NULL OR author_slug = $3) + ORDER BY created_at DESC + LIMIT $4 + `, [args.q || null, args.skill_type || null, args.author_slug || null, limit]); + return text({ skills: rows, count: rows.length }); + } + + if (name === "get_skill") { + const { rows } = await pool.query( + `SELECT * FROM vcap_skill_registry WHERE slug = $1 AND status = 'active'`, [args.slug] + ); + if (!rows.length) { + const e: any = new Error(`Skill "${args.slug}" not found`); e.code = -32602; throw e; + } + const row = rows[0]; + return text({ skill: { ...row, attestation: buildAttestation(row) } }); + } + + if (name === "submit_skill") { + const { name: skillName, author_name, content } = args; + if (!skillName || !author_name || !content) { + const e: any = new Error("name, author_name, and content are required"); e.code = -32602; throw e; + } + const slug = toSlug(skillName); + const authorSlug = toSlug(author_name); + const contentHash = sha256(content); + const attestationId = `sk-${Date.now().toString(36)}-${slug.slice(0, 8)}`; + const publishedAt = new Date().toISOString(); + const signature = signAttestation(slug, contentHash, authorSlug, publishedAt); + const tags = Array.isArray(args.tags) ? args.tags.map(String) : []; + + const { rows } = await pool.query(` + INSERT INTO vcap_skill_registry + (slug, name, summary, description, skill_type, author_name, author_slug, + content, content_hash, vcap_scope, attestation_id, signature, tags, version, registered_via) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,'mcp') + ON CONFLICT (slug) DO NOTHING + RETURNING * + `, [ + slug, skillName, + args.summary || skillName, + args.description || "", + args.skill_type || "agent_skill", + author_name, authorSlug, + content, contentHash, + args.vcap_scope || "skills:general:read:public", + attestationId, signature, tags, + args.version || "1.0.0" + ]); + + if (!rows.length) { + return text({ error: "A skill with that name already exists.", slug }); + } + return text({ registered: true, attestation: buildAttestation(rows[0]) }); + } + + if (name === "verify_integrity") { + const { slug, content } = args; + if (!slug || content === undefined) { + const e: any = new Error("slug and content are required"); e.code = -32602; throw e; + } + const { rows } = await pool.query( + `SELECT content_hash, name, author_name, created_at FROM vcap_skill_registry WHERE slug = $1 AND status = 'active'`, [slug] + ); + if (!rows.length) { + const e: any = new Error(`Skill "${slug}" not found`); e.code = -32602; throw e; + } + const computedHash = sha256(content); + const registryHash = rows[0].content_hash; + const match = computedHash === registryHash; + return text({ + slug, + skill_name: rows[0].name, + author_name: rows[0].author_name, + published_at: rows[0].created_at, + registry_hash: registryHash, + computed_hash: computedHash, + integrity: match ? "VERIFIED" : "TAMPERED", + message: match + ? "The content you hold matches the published attestation. No hidden content detected." + : "INTEGRITY FAILURE: The content does not match the published hash. This file has been modified." + }); + } + + const e: any = new Error(`Unknown tool: ${name}`); e.code = -32601; throw e; +} + +// ── Palette / styles (shared with agentify.help) ────────────────────────────── +const SK_CSS = ` + *,*::before,*::after{box-sizing:border-box} + body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#FDFAF4;color:#1A2B4A;margin:0;line-height:1.6} + a{color:#C9962A;text-decoration:none}a:hover{text-decoration:underline} + .nav{background:#1A2B4A;padding:.75rem 2rem;display:flex;align-items:center;gap:1.5rem;flex-wrap:wrap} + .nav-brand{color:#FDFAF4;font-weight:800;font-size:1.05rem;letter-spacing:-.01em} + .nav-brand span{color:#C9962A} + .nav a{color:rgba(253,250,244,.7);font-size:.88rem} + .nav a:hover{color:#FDFAF4;text-decoration:none} + .hero{background:#1A2B4A;color:#FDFAF4;padding:4rem 2rem 3.5rem} + .hero-inner{max-width:760px;margin:0 auto} + .hero-tag{display:inline-block;background:rgba(201,150,42,.2);color:#C9962A;border:1px solid rgba(201,150,42,.4);border-radius:4px;padding:.2rem .75rem;font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;font-weight:700;margin-bottom:1rem} + .hero h1{font-size:clamp(1.8rem,4vw,2.8rem);font-weight:900;margin:0 0 .75rem;line-height:1.15} + .hero p{font-size:1.05rem;color:rgba(253,250,244,.75);max-width:580px;margin:0} + .hero-actions{margin-top:1.75rem;display:flex;gap:.75rem;flex-wrap:wrap} + .btn{display:inline-block;padding:.6rem 1.4rem;border-radius:6px;font-weight:700;font-size:.88rem;cursor:pointer;border:none;text-decoration:none} + .btn-gold{background:#C9962A;color:#1A2B4A} + .btn-ghost{background:rgba(253,250,244,.1);color:#FDFAF4;border:1px solid rgba(253,250,244,.2)} + .btn-ghost:hover{background:rgba(253,250,244,.18);text-decoration:none} + .main{max-width:900px;margin:0 auto;padding:2.5rem 1.5rem} + .section-title{font-size:.72rem;text-transform:uppercase;letter-spacing:.12em;font-weight:700;color:#8A9099;margin-bottom:1.25rem} + .skill-card{background:#fff;border:1px solid rgba(26,43,74,.1);border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:.85rem;transition:box-shadow .15s} + .skill-card:hover{box-shadow:0 4px 18px rgba(26,43,74,.1)} + .skill-card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem} + .skill-name{font-size:1rem;font-weight:700;color:#1A2B4A;margin:0} + .skill-name a{color:#1A2B4A} + .skill-type{display:inline-block;background:rgba(201,150,42,.12);color:#8A6A1A;border-radius:3px;padding:.15rem .55rem;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em} + .skill-author{font-size:.85rem;color:#6A7A8A;margin:.35rem 0 0} + .skill-summary{font-size:.9rem;color:#4A5568;margin:.5rem 0 0} + .skill-meta{display:flex;align-items:center;gap:.75rem;margin-top:.75rem;flex-wrap:wrap} + .tag-pill{background:#F0EDE4;color:#5A6B7A;border-radius:3px;padding:.15rem .55rem;font-size:.72rem} + .hash-line{font-family:'SF Mono',monospace;font-size:.72rem;color:#8A9099;word-break:break-all} + pre{background:#F0EDE4;border:1px solid rgba(26,43,74,.1);border-radius:8px;padding:1.25rem 1.5rem;overflow-x:auto;font-size:.82rem;white-space:pre-wrap;word-break:break-word} + .verify-badge{display:inline-flex;align-items:center;gap:.4rem;padding:.3rem .8rem;border-radius:20px;font-size:.78rem;font-weight:700} + .verify-ok{background:#E6F4EA;color:#1A7A3A;border:1px solid #A8D5B5} + .footer{text-align:center;padding:2.5rem 1rem;font-size:.8rem;color:#8A9099;border-top:1px solid rgba(26,43,74,.08);margin-top:3rem} +`; + +const SK_NAV = ` +`; + +const SK_FOOTER = ` +`; + +function skPage(title: string, body: string) { + return ` + +${title} + + + + + +${SK_NAV}${body}${SK_FOOTER}`; +} + +// ── Route registration ──────────────────────────────────────────────────────── +export function registerSkillsAgentifyHelpRoutes(app: Express) { + // Run DB setup in background — routes register synchronously so they + // are in place before the Vite catch-all middleware is registered. + ensureSkillsTable().catch(e => console.error("[skills.agentify.help] DB setup error:", e)); + + function isSkills(req: Request): boolean { + const host = (req.headers["x-forwarded-host"] || req.headers.host || "") + .toString().split(",")[0].trim().replace(/:\d+$/, "").toLowerCase(); + return host.startsWith("skills."); + } + + // ── Landing page ───────────────────────────────────────────────────────── + app.get("/", async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const { rows } = await pool.query(` + SELECT slug, name, summary, skill_type, author_name, tags, created_at + FROM vcap_skill_registry WHERE status = 'active' + ORDER BY created_at DESC LIMIT 6 + `); + const cards = rows.map(r => ` +
+
+
+

${r.name}

+

by ${r.author_name}

+
+ ${(r.skill_type || "").replace(/_/g, " ")} +
+ ${r.summary ? `

${r.summary}

` : ""} +
+ ${(r.tags || []).map((t: string) => `${t}`).join("")} +
+
`).join("") || `

No skills published yet. Be first.

`; + + res.setHeader("Cache-Control", "public, max-age=60"); + res.send(skPage("skills.agentify.help — VCAP Skills Repository", ` +
+
+
VCAP Skills Repository
+

Skills that carry
their own proof.

+

Every skill published here is SHA-256 hashed and VCAP-attested on receipt. The file you receive socially — in plain text, in a message, in an email — either matches its hash or it doesn't. Math doesn't lie. No hidden fonts. No injected instructions. No back doors passed between humans in good faith.

+ +
+
+
+

Recently Published

+ ${cards} + ${rows.length > 0 ? `

View all skills →

` : ""} + +
+
+
🔐
+
Hash-attested integrity
+
Every skill has a SHA-256 fingerprint. Verify any file you receive — from anyone, over any channel — in one step.
+
+
+
📜
+
VCAP-governed conduct
+
Each skill carries a scope grammar (SGS) defining what it is authorized to do. The covenant is the operating agreement.
+
+
+
🤝
+
Social provenance
+
Skills of notable persons — craftspeople, healers, teachers — can be published, shared, and verified across generations.
+
+
+
+ `)); + }); + + // ── Browse all skills ───────────────────────────────────────────────────── + app.get("/skills", async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const page = Math.max(1, Number(req.query.page) || 1); + const limit = 24; + const offset = (page - 1) * limit; + const type = req.query.type as string || null; + const q = req.query.q as string || null; + + const { rows } = await pool.query(` + SELECT id AS registration_number, slug, name, summary, skill_type, author_name, author_slug, + content_hash, tags, version, created_at + FROM vcap_skill_registry WHERE status = 'active' + AND ($1::text IS NULL OR skill_type = $1) + AND ($2::text IS NULL OR name ILIKE '%'||$2||'%' OR author_name ILIKE '%'||$2||'%') + ORDER BY created_at DESC LIMIT $3 OFFSET $4 + `, [type, q, limit, offset]); + + const typeOpts = ["agent_skill", "persona_skill", "domain_skill", "craft_skill", "civic_skill"]; + const filterBar = `
+ ${typeOpts.map(t => `${t.replace(/_/g," ")}`).join("")} + ${type ? `× clear` : ""} +
`; + + const cards = rows.map(r => ` +
+
+
+

${r.name}

+

by ${r.author_name} · v${r.version} · #${r.registration_number}

+
+ ${(r.skill_type||"").replace(/_/g," ")} +
+ ${r.summary ? `

${r.summary}

` : ""} +
+ ${(r.tags||[]).map((t:string)=>`${t}`).join("")} + ${(r.content_hash||"").slice(0,30)}… +
+
`).join("") || `

No skills found.

`; + + res.setHeader("Cache-Control", "public, max-age=30"); + res.send(skPage("Browse Skills — skills.agentify.help", ` +
+
+

Skills Registry

+
+ + +
+
+
+
+ ${filterBar} +

${rows.length} skill${rows.length !== 1 ? "s" : ""} ${q ? `matching "${q}"` : type ? `— ${type.replace(/_/g," ")}` : "published"}

+ ${cards} + ${rows.length === limit ? `

Next page →

` : ""} +
+ `)); + }); + + // ── Individual skill page ───────────────────────────────────────────────── + app.get("/skills/:slug", async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const { rows } = await pool.query( + `SELECT * FROM vcap_skill_registry WHERE slug = $1 AND status = 'active'`, [req.params.slug] + ); + if (!rows.length) { + res.status(404).send(skPage("Skill Not Found — skills.agentify.help", `

Skill not found

Browse all skills →

`)); + return; + } + const r = rows[0]; + const attest = buildAttestation(r); + const contentPreview = (r.content || "").slice(0, 2000) + ((r.content || "").length > 2000 ? "\n\n… (truncated — download raw for full content)" : ""); + + res.setHeader("Cache-Control", "public, max-age=60"); + res.send(skPage(`${r.name} — skills.agentify.help`, ` +
+
+
+ ${(r.skill_type||"").replace(/_/g," ")} + v${r.version} +
+

${r.name}

+

by ${r.author_name} · Published ${new Date(r.created_at).toLocaleDateString("en-US",{year:"numeric",month:"long",day:"numeric"})}

+ ${r.summary ? `

${r.summary}

` : ""} +
+ Download Raw + Attestation JSON + Verify a Copy +
+
+
+
+
+

Integrity

+ ✓ VCAP Attested +

SHA-256 Content Hash

+

${r.content_hash}

+

VCAP Scope

+ ${r.vcap_scope} +

Attestation ID

+ ${r.attestation_id} +
+ + ${r.description ? `
+

Description

+

${r.description}

+
` : ""} + +
+

Skill Content

+

This is the hashed content. Download the raw file and verify its SHA-256 matches the hash above.

+
${contentPreview.replace(//g,">")}
+
+ + ${(r.tags||[]).length ? `
+ ${(r.tags||[]).map((t:string)=>`${t}`).join("")} +
` : ""} + +

+ Governed by WellSpr.ing covenant · + Browse all skills +

+
+ `)); + }); + + // ── Raw skill file ──────────────────────────────────────────────────────── + app.get("/skills/:slug/raw", async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const { rows } = await pool.query( + `SELECT name, content, content_hash, author_name, vcap_scope, attestation_id, created_at FROM vcap_skill_registry WHERE slug = $1 AND status = 'active'`, [req.params.slug] + ); + if (!rows.length) return res.status(404).type("text/plain").send("Skill not found"); + const r = rows[0]; + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.setHeader("Content-Disposition", `attachment; filename="${req.params.slug}.skill.txt"`); + res.setHeader("X-Content-Hash", r.content_hash); + res.setHeader("X-Attestation-Id", r.attestation_id); + res.setHeader("X-VCAP-Scope", r.vcap_scope); + res.setHeader("Cache-Control", "public, max-age=3600"); + res.send(r.content); + }); + + // ── Attestation JSON ────────────────────────────────────────────────────── + app.get("/skills/:slug/attest.json", async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const { rows } = await pool.query( + `SELECT * FROM vcap_skill_registry WHERE slug = $1 AND status = 'active'`, [req.params.slug] + ); + if (!rows.length) return res.status(404).json({ error: "Skill not found" }); + res.setHeader("Cache-Control", "public, max-age=3600"); + res.json(buildAttestation(rows[0])); + }); + + // ── Submit form (GET) ───────────────────────────────────────────────────── + app.get("/submit", (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + res.send(skPage("Publish a Skill — skills.agentify.help", ` +
+
+

Publish a Skill

+

Your content is hashed on receipt. A VCAP attestation is generated immediately. The hash is your integrity guarantee.

+
+
+
+
+
+ + + + + + + + +
+ +
+ +
+ `)); + }); + + // ── Verify page ─────────────────────────────────────────────────────────── + app.get("/verify", (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + res.send(skPage("Verify a Skill — skills.agentify.help", ` +
+
+

Verify a Skill File

+

Paste the content of any skill file you've received. We'll check it against the published hash. Any hidden line, hidden font, or injected instruction breaks the hash.

+
+
+
+ +
+ + + +
+ +
+ `)); + }); + + // ── API: submit skill ───────────────────────────────────────────────────── + app.post("/api/skills", express.json({ limit: "2mb" }), async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const ip = (req.headers["x-forwarded-for"] as string || req.socket.remoteAddress || "unknown").split(",")[0].trim(); + if (!rateCheck(`submit:${ip}`, 10, 3_600_000)) return res.status(429).json({ error: "Rate limit exceeded." }); + + const { name, author_name, content } = req.body || {}; + if (!name || !author_name || !content) return res.status(400).json({ error: "name, author_name, and content are required." }); + if (content.length > 500_000) return res.status(400).json({ error: "Content too large (max 500 KB)." }); + + const slug = toSlug(name); + if (!slug) return res.status(400).json({ error: "Could not generate a valid slug from that name." }); + const authorSlug = toSlug(author_name); + const contentHash = sha256(content); + const attestationId = `sk-${Date.now().toString(36)}-${slug.slice(0, 8)}`; + const publishedAt = new Date().toISOString(); + const signature = signAttestation(slug, contentHash, authorSlug, publishedAt); + const tags = Array.isArray(req.body.tags) ? req.body.tags.map(String) : []; + + try { + const { rows } = await pool.query(` + INSERT INTO vcap_skill_registry + (slug, name, summary, description, skill_type, author_name, author_slug, + content, content_hash, vcap_scope, attestation_id, signature, tags, version, registered_via) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,'web') + ON CONFLICT (slug) DO NOTHING + RETURNING id AS registration_number, slug, name, content_hash, attestation_id + `, [ + slug, name, + req.body.summary || name, + req.body.description || "", + req.body.skill_type || "agent_skill", + author_name, authorSlug, + content, contentHash, + req.body.vcap_scope || "skills:general:read:public", + attestationId, signature, tags, + req.body.version || "1.0.0" + ]); + + if (!rows.length) return res.status(409).json({ error: "A skill with that name already exists.", slug }); + res.status(201).json({ registered: true, slug: rows[0].slug, registration_number: rows[0].registration_number, content_hash: rows[0].content_hash, attestation_id: rows[0].attestation_id }); + } catch (e: any) { + res.status(500).json({ error: e.message }); + } + }); + + // ── API: list skills ────────────────────────────────────────────────────── + app.get("/api/skills", async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const limit = Math.min(Number(req.query.limit) || 20, 100); + const { rows } = await pool.query(` + SELECT id AS registration_number, slug, name, summary, skill_type, author_name, author_slug, + content_hash, vcap_scope, tags, version, created_at + FROM vcap_skill_registry WHERE status = 'active' + ORDER BY created_at DESC LIMIT $1 + `, [limit]); + res.setHeader("Cache-Control", "public, max-age=30"); + res.json({ skills: rows, count: rows.length }); + }); + + // ── API: get skill ──────────────────────────────────────────────────────── + app.get("/api/skills/:slug", async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const { rows } = await pool.query( + `SELECT * FROM vcap_skill_registry WHERE slug = $1 AND status = 'active'`, [req.params.slug] + ); + if (!rows.length) return res.status(404).json({ error: "Skill not found" }); + res.setHeader("Cache-Control", "public, max-age=60"); + res.json({ skill: rows[0], attestation: buildAttestation(rows[0]) }); + }); + + // ── API: verify integrity ───────────────────────────────────────────────── + app.post("/api/skills/:slug/verify", express.json({ limit: "2mb" }), async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const { content } = req.body || {}; + if (content === undefined) return res.status(400).json({ error: "content is required" }); + const { rows } = await pool.query( + `SELECT content_hash, name, author_name, created_at FROM vcap_skill_registry WHERE slug = $1 AND status = 'active'`, [req.params.slug] + ); + if (!rows.length) return res.status(404).json({ error: "Skill not found" }); + const computedHash = sha256(content); + const registryHash = rows[0].content_hash; + const match = computedHash === registryHash; + res.json({ + slug: req.params.slug, + skill_name: rows[0].name, + author_name: rows[0].author_name, + published_at: rows[0].created_at, + registry_hash: registryHash, + computed_hash: computedHash, + integrity: match ? "VERIFIED" : "TAMPERED", + message: match + ? "Content matches the published attestation. No tampering detected." + : "Content does NOT match the published hash. This file has been modified." + }); + }); + + // ── MCP endpoint ────────────────────────────────────────────────────────── + const SK_MCP_DISCOVERY = { + mcpVersion: "2025-03-26", + name: "skills-agentify-help", + displayName: "VCAP Skills Repository", + description: "Covenant-governed registry of VCAP-attested agent skills. Every skill carries a SHA-256 content hash — verify any file you receive. Tools: discover_skills, get_skill, submit_skill, verify_integrity.", + transport: [{ type: "http", url: "https://skills.agentify.help/mcp" }], + capabilities: { tools: {} }, + contact: { email: "ody@wellspr.ing", url: "https://skills.agentify.help" }, + legal: { termsOfService: "https://wellspr.ing/constitution" } + }; + + app.get("/.well-known/mcp.json", (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + res.setHeader("Cache-Control", "public, max-age=3600"); + res.json(SK_MCP_DISCOVERY); + }); + + app.get("/.well-known/ai-plugin.json", (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + res.setHeader("Cache-Control", "public, max-age=3600"); + res.json({ + schema_version: "v1", name_for_human: "VCAP Skills Repo", name_for_model: "vcap_skills_repo", + description_for_human: "Browse and verify covenant-attested agent skills.", description_for_model: "Search, retrieve, submit, and verify VCAP-attested skills. Use verify_integrity to check any skill file for hidden content or tampering.", + auth: { type: "none" }, api: { type: "openapi", url: "https://skills.agentify.help/.well-known/openapi.json" }, + contact_email: "ody@wellspr.ing", legal_info_url: "https://wellspr.ing/constitution" + }); + }); + + app.get("/robots.txt", (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + res.type("text/plain").send("User-agent: *\nAllow: /\nSitemap: https://skills.agentify.help/sitemap.xml\n"); + }); + + app.get("/sitemap.xml", async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const { rows } = await pool.query(`SELECT slug, created_at FROM vcap_skill_registry WHERE status = 'active' ORDER BY created_at DESC`); + const urls = [ + `https://skills.agentify.help/daily1.0`, + `https://skills.agentify.help/skillsdaily0.9`, + `https://skills.agentify.help/submitmonthly0.7`, + `https://skills.agentify.help/verifymonthly0.7`, + ...rows.map(r => `https://skills.agentify.help/skills/${r.slug}${new Date(r.created_at).toISOString().slice(0,10)}0.8`) + ]; + res.type("application/xml").send(`${urls.join("")}`); + }); + + app.get("/api/feed.json", async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const { rows } = await pool.query(`SELECT slug, name, summary, skill_type, author_name, content_hash, created_at FROM vcap_skill_registry WHERE status = 'active' ORDER BY created_at DESC LIMIT 50`); + res.setHeader("Cache-Control", "public, max-age=60"); + res.json({ feed: rows, count: rows.length, updated: new Date().toISOString() }); + }); + + app.post("/mcp", express.json({ limit: "1mb" }), async (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + const ip = (req.headers["x-forwarded-for"] as string || req.socket.remoteAddress || "unknown").split(",")[0].trim(); + const body = req.body; + if (!body || typeof body !== "object") { + return res.status(400).json({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }); + } + const { method, params, id } = body; + const ok = (result: any) => res.json({ jsonrpc: "2.0", id: id ?? null, result }); + const err = (code: number, message: string) => res.json({ jsonrpc: "2.0", id: id ?? null, error: { code, message } }); + res.setHeader("Content-Type", "application/json"); + res.setHeader("Cache-Control", "no-store"); + + switch (method) { + case "initialize": return ok({ protocolVersion: "2025-03-26", serverInfo: { name: "skills-agentify-help", version: "1.0.0" }, capabilities: { tools: {} } }); + case "notifications/initialized": return res.status(204).end(); + case "ping": return ok({}); + case "tools/list": return ok({ tools: SK_MCP_TOOLS }); + case "tools/call": { + const toolName = params?.name as string; + const args = params?.arguments ?? {}; + if (!toolName) return err(-32602, "params.name is required"); + if (toolName === "submit_skill" && !rateCheck(`mcp:${ip}`, 10, 3_600_000)) { + return ok({ content: [{ type: "text", text: JSON.stringify({ error: "Rate limit exceeded." }) }], isError: true }); + } + try { + const result = await handleSkillTool(toolName, args); + return ok(result); + } catch (e: any) { + if (e?.code) return err(e.code, e.message); + return err(-32603, e?.message || "Internal error"); + } + } + default: return err(-32601, `Method not found: ${method}`); + } + }); + + app.get("/mcp", (req: Request, res: Response, next) => { + if (!isSkills(req)) return next(); + res.json(SK_MCP_DISCOVERY); + }); + + console.log("[skills.agentify.help] Routes registered"); +}