feat: add routes/skills.ts

This commit is contained in:
gitadmin 2026-05-02 13:07:38 +00:00
parent 2805143fbb
commit fc49b0eac8

869
routes/skills.ts Normal file
View file

@ -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<string, { n: number; t: number }>();
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 = `
<nav class="nav">
<span class="nav-brand"><span>skills</span>.agentify.help</span>
<a href="https://skills.agentify.help/skills">Browse Skills</a>
<a href="https://skills.agentify.help/submit">Submit a Skill</a>
<a href="https://agentify.help">Agentify.Help</a>
<a href="https://skills.agentify.help/.well-known/mcp.json">MCP</a>
</nav>`;
const SK_FOOTER = `
<footer class="footer">
<p>skills.agentify.help · A <a href="https://wellspr.ing">WellSpr.ing</a> civic project · Governed by <a href="https://wellspr.ing/constitution">covenant</a></p>
<p style="margin-top:.35rem">Every skill carries a SHA-256 hash. Verify at <a href="https://skills.agentify.help/verify">skills.agentify.help/verify</a></p>
</footer>`;
function skPage(title: string, body: string) {
return `<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${title}</title>
<meta name="description" content="VCAP-attested agent skills repository. Every skill carries a SHA-256 hash — verify any file you receive in seconds.">
<meta property="og:title" content="${title}">
<meta property="og:description" content="Covenant-governed skills registry. Hash-attested. Tamper-evident.">
<meta property="og:url" content="https://skills.agentify.help">
<style>${SK_CSS}</style>
</head><body>${SK_NAV}${body}${SK_FOOTER}</body></html>`;
}
// ── 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 => `
<div class="skill-card">
<div class="skill-card-header">
<div>
<p class="skill-name"><a href="https://skills.agentify.help/skills/${r.slug}">${r.name}</a></p>
<p class="skill-author">by ${r.author_name}</p>
</div>
<span class="skill-type">${(r.skill_type || "").replace(/_/g, " ")}</span>
</div>
${r.summary ? `<p class="skill-summary">${r.summary}</p>` : ""}
<div class="skill-meta">
${(r.tags || []).map((t: string) => `<span class="tag-pill">${t}</span>`).join("")}
</div>
</div>`).join("") || `<p style="color:#8A9099">No skills published yet. Be first.</p>`;
res.setHeader("Cache-Control", "public, max-age=60");
res.send(skPage("skills.agentify.help — VCAP Skills Repository", `
<div class="hero">
<div class="hero-inner">
<div class="hero-tag">VCAP Skills Repository</div>
<h1>Skills that carry<br>their own proof.</h1>
<p>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.</p>
<div class="hero-actions">
<a href="https://skills.agentify.help/skills" class="btn btn-gold">Browse Skills</a>
<a href="https://skills.agentify.help/submit" class="btn btn-ghost">Publish a Skill</a>
<a href="https://skills.agentify.help/verify" class="btn btn-ghost">Verify a File</a>
</div>
</div>
</div>
<div class="main">
<p class="section-title">Recently Published</p>
${cards}
${rows.length > 0 ? `<p style="margin-top:1.5rem"><a href="https://skills.agentify.help/skills">View all skills →</a></p>` : ""}
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1.25rem;margin-top:3rem">
<div style="background:#fff;border:1px solid rgba(26,43,74,.1);border-radius:10px;padding:1.5rem">
<div style="font-size:1.5rem;margin-bottom:.75rem">🔐</div>
<div style="font-weight:700;margin-bottom:.5rem">Hash-attested integrity</div>
<div style="font-size:.88rem;color:#4A5568">Every skill has a SHA-256 fingerprint. Verify any file you receive from anyone, over any channel in one step.</div>
</div>
<div style="background:#fff;border:1px solid rgba(26,43,74,.1);border-radius:10px;padding:1.5rem">
<div style="font-size:1.5rem;margin-bottom:.75rem">📜</div>
<div style="font-weight:700;margin-bottom:.5rem">VCAP-governed conduct</div>
<div style="font-size:.88rem;color:#4A5568">Each skill carries a scope grammar (SGS) defining what it is authorized to do. The covenant is the operating agreement.</div>
</div>
<div style="background:#fff;border:1px solid rgba(26,43,74,.1);border-radius:10px;padding:1.5rem">
<div style="font-size:1.5rem;margin-bottom:.75rem">🤝</div>
<div style="font-weight:700;margin-bottom:.5rem">Social provenance</div>
<div style="font-size:.88rem;color:#4A5568">Skills of notable persons craftspeople, healers, teachers can be published, shared, and verified across generations.</div>
</div>
</div>
</div>
`));
});
// ── 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 = `<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1.75rem;align-items:center">
${typeOpts.map(t => `<a href="?type=${t}" style="padding:.3rem .8rem;border-radius:20px;font-size:.78rem;font-weight:600;${type===t?"background:#1A2B4A;color:#FDFAF4":"background:#F0EDE4;color:#4A5568"}">${t.replace(/_/g," ")}</a>`).join("")}
${type ? `<a href="/skills" style="padding:.3rem .8rem;border-radius:20px;font-size:.78rem;background:#F5E6C8;color:#8A6A1A;font-weight:600">× clear</a>` : ""}
</div>`;
const cards = rows.map(r => `
<div class="skill-card">
<div class="skill-card-header">
<div>
<p class="skill-name"><a href="https://skills.agentify.help/skills/${r.slug}">${r.name}</a></p>
<p class="skill-author">by ${r.author_name} · v${r.version} · #${r.registration_number}</p>
</div>
<span class="skill-type">${(r.skill_type||"").replace(/_/g," ")}</span>
</div>
${r.summary ? `<p class="skill-summary">${r.summary}</p>` : ""}
<div class="skill-meta">
${(r.tags||[]).map((t:string)=>`<span class="tag-pill">${t}</span>`).join("")}
<span class="hash-line">${(r.content_hash||"").slice(0,30)}</span>
</div>
</div>`).join("") || `<p style="color:#8A9099;padding:2rem 0">No skills found.</p>`;
res.setHeader("Cache-Control", "public, max-age=30");
res.send(skPage("Browse Skills — skills.agentify.help", `
<div class="hero" style="padding:2.5rem 2rem 2rem">
<div class="hero-inner">
<h1 style="font-size:1.8rem;margin-bottom:.5rem">Skills Registry</h1>
<form method="get" action="/skills" style="display:flex;gap:.5rem;margin-top:.75rem">
<input name="q" value="${q||""}" placeholder="Search by name or author…" style="flex:1;padding:.5rem .9rem;border-radius:6px;border:1px solid rgba(253,250,244,.2);background:rgba(253,250,244,.1);color:#FDFAF4;font-size:.9rem" data-testid="input-search-skills">
<button type="submit" class="btn btn-gold" data-testid="button-search">Search</button>
</form>
</div>
</div>
<div class="main">
${filterBar}
<p class="section-title">${rows.length} skill${rows.length !== 1 ? "s" : ""} ${q ? `matching "${q}"` : type ? `${type.replace(/_/g," ")}` : "published"}</p>
${cards}
${rows.length === limit ? `<p style="margin-top:1.5rem"><a href="?page=${page+1}${type?`&type=${type}`:""}">Next page →</a></p>` : ""}
</div>
`));
});
// ── 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", `<div class="main"><h2>Skill not found</h2><p><a href="/skills">Browse all skills →</a></p></div>`));
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`, `
<div class="hero" style="padding:2.5rem 2rem 2rem">
<div class="hero-inner">
<div style="display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem;flex-wrap:wrap">
<span class="skill-type">${(r.skill_type||"").replace(/_/g," ")}</span>
<span style="color:rgba(253,250,244,.5);font-size:.85rem">v${r.version}</span>
</div>
<h1 style="font-size:clamp(1.4rem,3vw,2rem);margin-bottom:.5rem">${r.name}</h1>
<p style="color:rgba(253,250,244,.7);font-size:.95rem">by ${r.author_name} · Published ${new Date(r.created_at).toLocaleDateString("en-US",{year:"numeric",month:"long",day:"numeric"})}</p>
${r.summary ? `<p style="margin-top:.75rem;color:rgba(253,250,244,.85);font-size:1rem">${r.summary}</p>` : ""}
<div style="display:flex;gap:.75rem;margin-top:1.25rem;flex-wrap:wrap">
<a href="https://skills.agentify.help/skills/${r.slug}/raw" class="btn btn-gold" download data-testid="button-download-raw">Download Raw</a>
<a href="https://skills.agentify.help/skills/${r.slug}/attest.json" class="btn btn-ghost" data-testid="link-attestation">Attestation JSON</a>
<a href="https://skills.agentify.help/verify" class="btn btn-ghost" data-testid="link-verify">Verify a Copy</a>
</div>
</div>
</div>
<div class="main">
<div style="background:#fff;border:1px solid rgba(26,43,74,.1);border-radius:10px;padding:1.5rem;margin-bottom:1.5rem">
<p class="section-title">Integrity</p>
<span class="verify-badge verify-ok"> VCAP Attested</span>
<p style="margin:.75rem 0 .35rem;font-size:.88rem;font-weight:600">SHA-256 Content Hash</p>
<p class="hash-line" data-testid="text-content-hash">${r.content_hash}</p>
<p style="margin:1rem 0 .35rem;font-size:.88rem;font-weight:600">VCAP Scope</p>
<code style="font-size:.85rem;background:#F0EDE4;padding:3px 7px;border-radius:3px">${r.vcap_scope}</code>
<p style="margin:1rem 0 .35rem;font-size:.88rem;font-weight:600">Attestation ID</p>
<code style="font-size:.82rem;background:#F0EDE4;padding:3px 7px;border-radius:3px">${r.attestation_id}</code>
</div>
${r.description ? `<div style="background:#fff;border:1px solid rgba(26,43,74,.1);border-radius:10px;padding:1.5rem;margin-bottom:1.5rem">
<p class="section-title">Description</p>
<p style="font-size:.95rem;color:#2A3B4A;white-space:pre-wrap">${r.description}</p>
</div>` : ""}
<div style="background:#fff;border:1px solid rgba(26,43,74,.1);border-radius:10px;padding:1.5rem;margin-bottom:1.5rem">
<p class="section-title">Skill Content</p>
<p style="font-size:.8rem;color:#8A9099;margin-bottom:.75rem">This is the hashed content. Download the raw file and verify its SHA-256 matches the hash above.</p>
<pre data-testid="pre-skill-content">${contentPreview.replace(/</g,"&lt;").replace(/>/g,"&gt;")}</pre>
</div>
${(r.tags||[]).length ? `<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1.5rem">
${(r.tags||[]).map((t:string)=>`<span class="tag-pill">${t}</span>`).join("")}
</div>` : ""}
<p style="font-size:.82rem;color:#8A9099">
Governed by <a href="${r.covenant_url}">WellSpr.ing covenant</a> ·
<a href="https://skills.agentify.help/skills">Browse all skills</a>
</p>
</div>
`));
});
// ── 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", `
<div class="hero" style="padding:2.5rem 2rem 2rem">
<div class="hero-inner">
<h1 style="font-size:1.8rem">Publish a Skill</h1>
<p style="color:rgba(253,250,244,.75)">Your content is hashed on receipt. A VCAP attestation is generated immediately. The hash is your integrity guarantee.</p>
</div>
</div>
<div class="main" style="max-width:680px">
<form method="post" action="/api/skills" id="submit-form">
<div style="display:grid;gap:1.25rem">
<label style="display:grid;gap:.4rem">
<span style="font-weight:600;font-size:.88rem">Skill name *</span>
<input name="name" required placeholder="e.g. Sourdough Bread Baking by Martha Chen" style="padding:.6rem .9rem;border-radius:6px;border:1px solid rgba(26,43,74,.2);font-size:.92rem;background:#fff" data-testid="input-skill-name">
</label>
<label style="display:grid;gap:.4rem">
<span style="font-weight:600;font-size:.88rem">Author full name *</span>
<input name="author_name" required placeholder="Martha Chen" style="padding:.6rem .9rem;border-radius:6px;border:1px solid rgba(26,43,74,.2);font-size:.92rem;background:#fff" data-testid="input-author-name">
</label>
<label style="display:grid;gap:.4rem">
<span style="font-weight:600;font-size:.88rem">Skill type</span>
<select name="skill_type" style="padding:.6rem .9rem;border-radius:6px;border:1px solid rgba(26,43,74,.2);font-size:.92rem;background:#fff" data-testid="select-skill-type">
<option value="agent_skill">Agent Skill</option>
<option value="persona_skill">Persona Skill</option>
<option value="domain_skill">Domain Skill</option>
<option value="craft_skill">Craft Skill</option>
<option value="civic_skill">Civic Skill</option>
</select>
</label>
<label style="display:grid;gap:.4rem">
<span style="font-weight:600;font-size:.88rem">One-sentence summary</span>
<input name="summary" placeholder="What this skill enables in one sentence" style="padding:.6rem .9rem;border-radius:6px;border:1px solid rgba(26,43,74,.2);font-size:.92rem;background:#fff" data-testid="input-summary">
</label>
<label style="display:grid;gap:.4rem">
<span style="font-weight:600;font-size:.88rem">VCAP scope <span style="color:#8A9099;font-weight:400">(SGS string)</span></span>
<input name="vcap_scope" placeholder="skills:general:read:public" value="skills:general:read:public" style="padding:.6rem .9rem;border-radius:6px;border:1px solid rgba(26,43,74,.2);font-size:.92rem;background:#fff;font-family:monospace" data-testid="input-vcap-scope">
</label>
<label style="display:grid;gap:.4rem">
<span style="font-weight:600;font-size:.88rem">Tags <span style="color:#8A9099;font-weight:400">(comma-separated)</span></span>
<input name="tags" placeholder="herbalism, healing, plants" style="padding:.6rem .9rem;border-radius:6px;border:1px solid rgba(26,43,74,.2);font-size:.92rem;background:#fff" data-testid="input-tags">
</label>
<label style="display:grid;gap:.4rem">
<span style="font-weight:600;font-size:.88rem">Skill content * <span style="color:#8A9099;font-weight:400">(plain text / Markdown)</span></span>
<textarea name="content" required rows="14" placeholder="Write or paste the full skill file content here. This is what gets hashed." style="padding:.6rem .9rem;border-radius:6px;border:1px solid rgba(26,43,74,.2);font-size:.88rem;font-family:'SF Mono',monospace;resize:vertical;background:#fff" data-testid="textarea-skill-content"></textarea>
</label>
<label style="display:grid;gap:.4rem">
<span style="font-weight:600;font-size:.88rem">Full description <span style="color:#8A9099;font-weight:400">(optional)</span></span>
<textarea name="description" rows="4" placeholder="Longer description of what this skill does and when to use it" style="padding:.6rem .9rem;border-radius:6px;border:1px solid rgba(26,43,74,.2);font-size:.88rem;resize:vertical;background:#fff" data-testid="textarea-description"></textarea>
</label>
</div>
<button type="submit" class="btn btn-gold" style="margin-top:1.5rem;padding:.75rem 2rem;font-size:.95rem" data-testid="button-submit-skill">Publish &amp; Attest</button>
</form>
<script>
document.getElementById('submit-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const tags = (fd.get('tags')||'').split(',').map(t=>t.trim()).filter(Boolean);
const body = { name: fd.get('name'), author_name: fd.get('author_name'), content: fd.get('content'),
summary: fd.get('summary'), description: fd.get('description'),
skill_type: fd.get('skill_type'), vcap_scope: fd.get('vcap_scope'), tags };
const btn = document.querySelector('[data-testid="button-submit-skill"]');
btn.textContent = 'Publishing…'; btn.disabled = true;
const res = await fetch('/api/skills', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
const data = await res.json();
if (data.slug) { window.location.href = '/skills/' + data.slug; }
else { alert(data.error || 'Submission failed'); btn.textContent = 'Publish & Attest'; btn.disabled = false; }
});
</script>
</div>
`));
});
// ── Verify page ───────────────────────────────────────────────────────────
app.get("/verify", (req: Request, res: Response, next) => {
if (!isSkills(req)) return next();
res.send(skPage("Verify a Skill — skills.agentify.help", `
<div class="hero" style="padding:2.5rem 2rem 2rem">
<div class="hero-inner">
<h1 style="font-size:1.8rem">Verify a Skill File</h1>
<p style="color:rgba(253,250,244,.75)">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.</p>
</div>
</div>
<div class="main" style="max-width:680px">
<div id="verify-result" style="display:none;margin-bottom:1.5rem"></div>
<div style="display:grid;gap:1.25rem">
<label style="display:grid;gap:.4rem">
<span style="font-weight:600;font-size:.88rem">Skill slug</span>
<input id="v-slug" placeholder="sourdough-bread-baking-by-martha-chen" style="padding:.6rem .9rem;border-radius:6px;border:1px solid rgba(26,43,74,.2);font-size:.92rem;font-family:monospace" data-testid="input-verify-slug">
</label>
<label style="display:grid;gap:.4rem">
<span style="font-weight:600;font-size:.88rem">File content you received</span>
<textarea id="v-content" rows="14" placeholder="Paste the full skill file content here…" style="padding:.6rem .9rem;border-radius:6px;border:1px solid rgba(26,43,74,.2);font-size:.88rem;font-family:'SF Mono',monospace;resize:vertical" data-testid="textarea-verify-content"></textarea>
</label>
<button onclick="doVerify()" class="btn btn-gold" style="padding:.75rem 2rem;font-size:.95rem;width:fit-content" data-testid="button-verify">Verify Integrity</button>
</div>
<script>
async function doVerify() {
const slug = document.getElementById('v-slug').value.trim();
const content = document.getElementById('v-content').value;
const btn = document.querySelector('[data-testid="button-verify"]');
btn.textContent = 'Verifying…'; btn.disabled = true;
const res = await fetch('/api/skills/' + slug + '/verify', {
method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ content })
});
const data = await res.json();
const el = document.getElementById('verify-result');
el.style.display = 'block';
if (data.integrity === 'VERIFIED') {
el.innerHTML = '<div class="verify-badge verify-ok" style="font-size:.95rem;padding:.6rem 1.25rem">✓ VERIFIED — ' + data.message + '</div>';
} else {
el.innerHTML = '<div class="verify-badge" style="background:#FEE;color:#C00;border:1px solid #F5C6C6;font-size:.95rem;padding:.6rem 1.25rem">✗ INTEGRITY FAILURE — ' + data.message + '</div>';
}
btn.textContent = 'Verify Integrity'; btn.disabled = false;
}
</script>
</div>
`));
});
// ── 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 = [
`<url><loc>https://skills.agentify.help/</loc><changefreq>daily</changefreq><priority>1.0</priority></url>`,
`<url><loc>https://skills.agentify.help/skills</loc><changefreq>daily</changefreq><priority>0.9</priority></url>`,
`<url><loc>https://skills.agentify.help/submit</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>`,
`<url><loc>https://skills.agentify.help/verify</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>`,
...rows.map(r => `<url><loc>https://skills.agentify.help/skills/${r.slug}</loc><lastmod>${new Date(r.created_at).toISOString().slice(0,10)}</lastmod><priority>0.8</priority></url>`)
];
res.type("application/xml").send(`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join("")}</urlset>`);
});
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");
}