agentify-help/routes/skills.ts

869 lines
51 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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");
}