From bcb5afa82026970cdd50642fec14c5000bc6eedb Mon Sep 17 00:00:00 2001 From: gitadmin Date: Sat, 2 May 2026 13:07:40 +0000 Subject: [PATCH] feat: add routes/steward.ts --- routes/steward.ts | 1444 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1444 insertions(+) create mode 100644 routes/steward.ts diff --git a/routes/steward.ts b/routes/steward.ts new file mode 100644 index 0000000..c1cc666 --- /dev/null +++ b/routes/steward.ts @@ -0,0 +1,1444 @@ +/** + * PersonaForge Stewardship Dashboard + * + * The steward-facing workspace for vetting corpus artifacts before a build. + * Not the admin panel — this is the person responsible for the corpus doing + * their actual review work, with Ody as a research assistant. + * + * Routes (agentify.help host, dashboard-token protected): + * GET /steward/:slug — SSR dashboard + * GET /api/steward/:slug/info — subject + stats + * GET /api/steward/:slug/artifacts — list artifacts + curation + * PATCH /api/steward/:slug/artifacts/:artifact_id — update curation status + * POST /api/steward/:slug/artifacts/:artifact_id/ody-ask — Ody analysis + * POST /api/steward/:slug/corpus/fetch — trigger essay fetch + * POST /api/steward/:slug/corpus/build — build approved-only corpus + * GET /api/steward/:slug/dashboard-token — admin: get/generate token + */ + +import type { Express, Request, Response } from "express"; +import { pool } from "../db"; +import crypto from "crypto"; +import Anthropic from "@anthropic-ai/sdk"; + +const ADMIN_KEY = process.env.ADMIN_KEY || "b0db7a87384fc814b0f46ea7bdc6ab6a81152be5b098718b"; +const claude = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + +// ── Schema migrations ───────────────────────────────────────────────────────── +export async function initStewardDash() { + // Curation columns on artifacts + await pool.query(`ALTER TABLE personaforge_artifacts ADD COLUMN IF NOT EXISTS curation_status TEXT NOT NULL DEFAULT 'pending'`); + await pool.query(`ALTER TABLE personaforge_artifacts ADD COLUMN IF NOT EXISTS curation_note TEXT`); + await pool.query(`ALTER TABLE personaforge_artifacts ADD COLUMN IF NOT EXISTS ody_verdict TEXT`); + // Manual artifact content (bypasses URL fetch — used for injected bio facts, Wikipedia excerpts, etc.) + await pool.query(`ALTER TABLE personaforge_artifacts ADD COLUMN IF NOT EXISTS content_text TEXT`); + // Dashboard token stored on personaforge_subjects (guaranteed to exist for all subjects) + await pool.query(`ALTER TABLE personaforge_subjects ADD COLUMN IF NOT EXISTS dashboard_token TEXT`); + // Biographical baseline facts (structured markdown, highest-priority corpus chunk, not curatable) + await pool.query(`ALTER TABLE personaforge_subjects ADD COLUMN IF NOT EXISTS bio_facts_md TEXT`); + // PTP attestation metadata for bio_facts (who wrote it and under what token/scope) + await pool.query(`ALTER TABLE personaforge_subjects ADD COLUMN IF NOT EXISTS bio_facts_ptp_token_id TEXT`); + await pool.query(`ALTER TABLE personaforge_subjects ADD COLUMN IF NOT EXISTS bio_facts_attested_at TIMESTAMPTZ`); + await pool.query(`ALTER TABLE personaforge_subjects ADD COLUMN IF NOT EXISTS bio_facts_vcap_uri TEXT`); + // Wikipedia/secondary source summary (skepticism-weighted; separate from primary corpus) + await pool.query(`ALTER TABLE personaforge_subjects ADD COLUMN IF NOT EXISTS wiki_summary_md TEXT`); + // PTP attestation metadata for wiki summary + await pool.query(`ALTER TABLE personaforge_subjects ADD COLUMN IF NOT EXISTS wiki_ptp_token_id TEXT`); + await pool.query(`ALTER TABLE personaforge_subjects ADD COLUMN IF NOT EXISTS wiki_attested_at TIMESTAMPTZ`); + // Corpus consult log (PTP injection events, QA sessions) + await pool.query(` + CREATE TABLE IF NOT EXISTS corpus_consult_log ( + id SERIAL PRIMARY KEY, + slug TEXT NOT NULL, + event_type TEXT NOT NULL, + token_id TEXT, + scope TEXT, + purpose TEXT, + attested_at TEXT, + session_notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() + ) + `); + // Also add to registry for forward-compat (new stewards via web form) + await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS dashboard_token TEXT`).catch(() => {}); + console.log("[StewardDash] Schema columns ready"); +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── +function isAgentify(req: Request): boolean { + const candidates = [ + req.headers["x-forwarded-host"], + req.headers["x-geo-node-host"], + req.hostname, + req.headers["host"], + ].flat().filter(Boolean).map((h: any) => h.toString().toLowerCase().split(",")[0].trim()); + return candidates.some(h => h.includes("agentify") && !h.startsWith("skills.")); +} + +async function validateStewardToken(slug: string, token: string | undefined): Promise { + if (!token) return false; + // Primary source: personaforge_subjects (exists for all subjects including admin-seeded) + const { rows } = await pool.query( + `SELECT dashboard_token FROM personaforge_subjects WHERE slug = $1`, + [slug] + ); + if (rows.length > 0 && rows[0].dashboard_token === token) return true; + // Fallback: agentify_subject_registry (for stewards registered via web form) + const { rows: reg } = await pool.query( + `SELECT dashboard_token FROM agentify_subject_registry WHERE subject_slug = $1`, + [slug] + ).catch(() => ({ rows: [] })); + return reg.length > 0 && reg[0].dashboard_token === token; +} + +async function getOrCreateDashboardToken(slug: string): Promise { + // Primary: personaforge_subjects + const { rows } = await pool.query( + `SELECT dashboard_token FROM personaforge_subjects WHERE slug = $1`, [slug] + ); + if (rows[0]?.dashboard_token) return rows[0].dashboard_token; + const token = crypto.randomBytes(24).toString("hex"); + // Store in personaforge_subjects + const updated = await pool.query( + `UPDATE personaforge_subjects SET dashboard_token = $2, updated_at = NOW() WHERE slug = $1 RETURNING slug`, + [slug, token] + ); + if ((updated.rowCount || 0) === 0) throw new Error(`Subject '${slug}' not found in personaforge_subjects`); + // Also sync to agentify_subject_registry if that row exists + await pool.query( + `UPDATE agentify_subject_registry SET dashboard_token = $2, updated_at = NOW() WHERE subject_slug = $1`, + [slug, token] + ).catch(() => {}); + return token; +} + +async function fetchEssayText(url: string): Promise { + try { + const html = await fetch(url, { + headers: { "User-Agent": "WellSpr.ing PersonaForge/1.0" }, + signal: AbortSignal.timeout(15000), + }).then(r => r.text()); + return html + .replace(/]*>[\s\S]*?<\/script>/gi, " ") + .replace(/]*>[\s\S]*?<\/style>/gi, " ") + .replace(/<[^>]+>/g, " ") + .replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, " ") + .replace(/\s+/g, " ").trim(); + } catch { + return ""; + } +} + +// ── SSR Dashboard page ──────────────────────────────────────────────────────── +function dashboardPage(slug: string, subjectName: string): string { + return ` + + + + +${subjectName} — Stewardship Dashboard | Agentify.Help + + + + + +
+
+ AGENTIFY.HELP +
+
${subjectName}
+
Corpus Stewardship Dashboard
+
+
+
+ + +
+
+ + +
+
Total
+
Approved
+
Rejected
+
Flagged
+
Pending
+
+
0 of 0 reviewed
+
+
+
+ + +
+
+

Biographical Baseline

+ Loading… + ▼ show +
+
+
+

Ground-Truth Facts — injected as first corpus chunk, highest priority, not curatable

+ +
+ + + +
+
+
+

Wikipedia / Secondary Sources — skepticism-weighted; separate from primary corpus

+ +
+ + + +
+
+
+
+ + +
+ + + + + +
+ +
+
+ + +
+
Loading corpus artifacts…
+
+ + +
+ Building corpus… +
+ +
+ + + + + + +`; +} + +// ── Routes ──────────────────────────────────────────────────────────────────── +// NOTE: This function is intentionally synchronous so that app.get("/steward/:slug") +// is registered before serveSpaFallback() in the production middleware stack. +// initStewardDash() runs DB schema migrations in the background — the route +// handlers themselves are safe to execute once the DB is available. +export function registerStewardRoutes(app: Express) { + // Run schema migrations in the background; do NOT await before registering routes + initStewardDash().catch((e: any) => console.error("[StewardDash] init error:", e.message)); + + // ── GET /steward/:slug — SSR dashboard ────────────────────────────────── + // Note: host guard removed — the dashboard token is the access control. + // Subjects may exist in either personaforge_subjects (admin-seeded) or + // agentify_subject_registry (registered via web form). We check both and + // auto-promote registry-only subjects into personaforge_subjects on first access. + app.get("/steward/:slug", async (req: Request, res: Response) => { + const { slug } = req.params; + const token = req.query.token as string; + + const notFoundPage = `Not found + +

Not found

No subject found for slug: ${slug}

`; + + const accessDeniedPage = `Access denied + +

Access denied

This dashboard requires a valid stewardship token. Use the link from your confirmation email, or contact ody@wellspr.ing.

`; + + try { + // 1. Look up in personaforge_subjects (primary/canonical source) + let { rows: pf } = await pool.query( + `SELECT subject_name, build_status, dashboard_token FROM personaforge_subjects WHERE slug = $1`, [slug] + ); + + // 2. If not found in personaforge_subjects, check agentify_subject_registry + // (stewards who registered via the web form end up here) + if (!pf[0]) { + const { rows: reg } = await pool.query( + `SELECT subject_name, subject_domain, steward_name, steward_email, dashboard_token, status + FROM agentify_subject_registry WHERE subject_slug = $1`, [slug] + ).catch(() => ({ rows: [] as any[] })); + + if (!reg[0]) { + return res.status(404).send(notFoundPage); + } + + const r = reg[0]; + + // Auto-promote into personaforge_subjects so all dashboard features work + await pool.query( + `INSERT INTO personaforge_subjects (slug, subject_name, domain, build_status, dashboard_token) + VALUES ($1, $2, $3, 'active', $4) + ON CONFLICT (slug) DO UPDATE + SET subject_name = EXCLUDED.subject_name, + domain = EXCLUDED.domain, + dashboard_token = COALESCE(personaforge_subjects.dashboard_token, EXCLUDED.dashboard_token), + updated_at = NOW()`, + [slug, r.subject_name, r.subject_domain || "general", r.dashboard_token || null] + ).catch((e: any) => console.warn("[StewardDash] auto-promote error:", e.message)); + + // Re-fetch so the rest of the handler sees the row + const refetch = await pool.query( + `SELECT subject_name, build_status, dashboard_token FROM personaforge_subjects WHERE slug = $1`, [slug] + ); + pf = refetch.rows; + if (!pf[0]) { + // Fallback: use registry data directly + const validToken = r.dashboard_token; + if (!validToken || token !== validToken) return res.status(403).send(accessDeniedPage); + return res.send(dashboardPage(slug, r.subject_name)); + } + } + + const subjectName = pf[0].subject_name; + const validToken = pf[0].dashboard_token; + if (!validToken || token !== validToken) return res.status(403).send(accessDeniedPage); + + return res.send(dashboardPage(slug, subjectName)); + } catch (e: any) { + console.error("[StewardDash] /steward/:slug error:", e.message); + return res.status(500).send(`

Error

Something went wrong loading this dashboard. Please try again.

`); + } + }); + + // ── GET /api/steward/:slug/info ────────────────────────────────────────── + app.get("/api/steward/:slug/info", async (req: Request, res: Response) => { + const { slug } = req.params; + const token = req.query.token as string || req.body?.token; + if (!await validateStewardToken(slug, token)) return res.status(403).json({ error: "Invalid token" }); + + const { rows: pf } = await pool.query(`SELECT * FROM personaforge_subjects WHERE slug = $1`, [slug]); + if (!pf[0]) return res.status(404).json({ error: "Subject not found" }); + + const { rows: counts } = await pool.query( + `SELECT curation_status, COUNT(*) as n FROM personaforge_artifacts WHERE subject_slug = $1 GROUP BY curation_status`, [slug] + ); + const stats: Record = { pending: 0, approved: 0, rejected: 0, flagged: 0 }; + counts.forEach(r => { stats[r.curation_status] = parseInt(r.n); }); + + res.json({ subject: pf[0], stats }); + }); + + // ── GET /api/steward/:slug/artifacts ──────────────────────────────────── + app.get("/api/steward/:slug/artifacts", async (req: Request, res: Response) => { + const { slug } = req.params; + const token = req.query.token as string; + if (!await validateStewardToken(slug, token)) return res.status(403).json({ error: "Invalid token" }); + + const { rows } = await pool.query( + `SELECT artifact_id, title, source_url, pub_date, artifact_type, word_count, chunk_count, curation_status, curation_note, ody_verdict, created_at + FROM personaforge_artifacts WHERE subject_slug = $1 ORDER BY pub_date DESC NULLS LAST, title ASC`, + [slug] + ); + res.json({ artifacts: rows }); + }); + + // ── PATCH /api/steward/:slug/artifacts/:artifact_id ───────────────────── + app.patch("/api/steward/:slug/artifacts/:artifact_id", async (req: Request, res: Response) => { + const { slug, artifact_id } = req.params; + const { token, curation_status, curation_note } = req.body; + if (!await validateStewardToken(slug, token)) return res.status(403).json({ error: "Invalid token" }); + + const allowed = ["pending", "approved", "rejected", "flagged"]; + if (!allowed.includes(curation_status)) return res.status(400).json({ error: "Invalid curation_status" }); + + await pool.query( + `UPDATE personaforge_artifacts SET curation_status = $1, curation_note = $2 WHERE artifact_id = $3 AND subject_slug = $4`, + [curation_status, curation_note || null, artifact_id, slug] + ); + res.json({ ok: true }); + }); + + // ── POST /api/steward/:slug/artifacts/:artifact_id/ody-ask ────────────── + app.post("/api/steward/:slug/artifacts/:artifact_id/ody-ask", async (req: Request, res: Response) => { + const { slug, artifact_id } = req.params; + const { token } = req.body; + if (!await validateStewardToken(slug, token)) return res.status(403).json({ error: "Invalid token" }); + + // Get artifact + const { rows } = await pool.query( + `SELECT title, source_url, word_count, artifact_type FROM personaforge_artifacts WHERE artifact_id = $1 AND subject_slug = $2`, + [artifact_id, slug] + ); + if (!rows[0]) return res.status(404).json({ error: "Artifact not found" }); + const art = rows[0]; + + // Get subject info + const { rows: pf } = await pool.query(`SELECT subject_name, domain FROM personaforge_subjects WHERE slug = $1`, [slug]); + const subject = pf[0] || { subject_name: slug, domain: "" }; + + // Fetch essay text (first ~3000 words worth) + let essayText = ""; + if (art.source_url) { + const raw = await fetchEssayText(art.source_url); + essayText = raw.substring(0, 12000); // ~3000 words + } + + if (!essayText) { + return res.json({ verdict: `Verdict: Flag\n\nCould not fetch the essay text from ${art.source_url || "the source URL"}. You may want to review this one manually before including it.` }); + } + + const prompt = `You are helping a steward vet corpus material for an AI persona based on ${subject.subject_name}. + +The steward is reviewing each piece of source material to decide whether it should be included in the corpus that will train and ground the persona. + +**Essay title:** "${art.title}" +**Type:** ${art.artifact_type || "essay"} +**Word count:** ${art.word_count || "unknown"} + +**Essay text (excerpt):** +${essayText} + +Please give a concise vetting assessment (3–5 sentences). Address: +1. Is this representative of ${subject.subject_name}'s actual thinking and voice? +2. Is the content high signal — substantive ideas, not just announcements or filler? +3. Any concerns about quality, accuracy, or relevance to the corpus? + +Then give your verdict on a new line: "Verdict: Approve", "Verdict: Flag", or "Verdict: Reject".`; + + try { + const msg = await claude.messages.create({ + model: "claude-haiku-4-5", + max_tokens: 400, + messages: [{ role: "user", content: prompt }], + }); + const verdict = (msg.content[0] as any).text || ""; + // Store verdict + await pool.query( + `UPDATE personaforge_artifacts SET ody_verdict = $1 WHERE artifact_id = $2`, + [verdict, artifact_id] + ); + res.json({ verdict }); + } catch (e: any) { + console.error("[StewardDash] Ody ask error:", e.message); + res.status(500).json({ error: "Claude error: " + e.message }); + } + }); + + // ── POST /api/steward/:slug/corpus/fetch ──────────────────────────────── + // Delegates to PersonaForge fetch (admin-side), authorized via steward token + app.post("/api/steward/:slug/corpus/fetch", async (req: Request, res: Response) => { + const { slug } = req.params; + const { token } = req.body; + if (!await validateStewardToken(slug, token)) return res.status(403).json({ error: "Invalid token" }); + + // Call the PersonaForge fetch endpoint internally (same process) + try { + const { rows: pf } = await pool.query( + `SELECT source_url FROM personaforge_subjects WHERE slug = $1`, [slug] + ); + if (!pf[0]?.source_url) return res.status(400).json({ error: "No source URL configured for this subject" }); + + const sourceUrl = pf[0].source_url; + const html = await fetch(sourceUrl, { headers: { "User-Agent": "WellSpr.ing PersonaForge/1.0" } }).then(r => r.text()); + const base = new URL(sourceUrl); + const found: { slug: string; title: string; url: string }[] = []; + const re = /]*>([^<]+)<\/a>/gi; + let m: RegExpExecArray | null; + while ((m = re.exec(html)) !== null) { + const href = m[1].trim(); + const title = m[2].trim(); + if (!title || title.length < 3) continue; + try { + const full = new URL(href, base).href; + const artSlug = href.replace(/.*\//, "").replace(".html", "").toLowerCase().replace(/[^a-z0-9]+/g, "-"); + if (!found.find(r => r.slug === artSlug)) found.push({ slug: artSlug, title, url: full }); + } catch {} + } + + let inserted = 0; + for (const item of found) { + const artifactId = `${slug}::${item.slug}`; + const result = await pool.query( + `INSERT INTO personaforge_artifacts (subject_slug, artifact_id, title, source_url, artifact_type, curation_status) + VALUES ($1, $2, $3, $4, 'essay', 'pending') + ON CONFLICT (artifact_id) DO NOTHING`, + [slug, artifactId, item.title, item.url] + ); + if (result.rowCount && result.rowCount > 0) inserted++; + } + + await pool.query(`UPDATE personaforge_subjects SET essay_count = $2, updated_at = NOW() WHERE slug = $1`, [slug, found.length]); + res.json({ ok: true, count: found.length, inserted }); + } catch (e: any) { + console.error("[StewardDash] Fetch error:", e.message); + res.status(500).json({ error: e.message }); + } + }); + + // ── POST /api/steward/:slug/corpus/build ──────────────────────────────── + // Triggers a build using only approved artifacts + app.post("/api/steward/:slug/corpus/build", async (req: Request, res: Response) => { + const { slug } = req.params; + const { token } = req.body; + if (!await validateStewardToken(slug, token)) return res.status(403).json({ error: "Invalid token" }); + + // Get approved artifact IDs + const { rows: approved } = await pool.query( + `SELECT artifact_id FROM personaforge_artifacts WHERE subject_slug = $1 AND curation_status = 'approved'`, [slug] + ); + if (approved.length === 0) return res.status(400).json({ error: "No approved artifacts to build" }); + + // Delegate to personaforge build endpoint (internal HTTP call via admin key) + try { + const bundleId = crypto.randomUUID(); + await pool.query( + `UPDATE personaforge_subjects SET build_status = 'building', bundle_id = $2, build_log = '', updated_at = NOW() WHERE slug = $1`, + [slug, bundleId] + ); + + res.json({ ok: true, bundle_id: bundleId, approved_count: approved.length, message: "Build started — connect to SSE log stream for progress" }); + + // Async build (fire and forget — steward watches via SSE) + setImmediate(() => runApprovedBuild(slug, bundleId, approved.map(r => r.artifact_id))); + } catch (e: any) { + res.status(500).json({ error: e.message }); + } + }); + + // ── POST /api/steward/:slug/bio-facts/ptp ─────────────────────────────── + // PTP-authenticated corpus injection. Any AI agent holding a valid WellSpr.ing + // PTP token with scope corpus:write:personaforge:{slug} (or :*) may inject + // verified biographical facts and have them stamped with their attestation. + // + // Intended use: a Claude session runs the QA skill against a persona, verifies + // facts against primary sources, then writes the verified facts back to the + // corpus with a PTP-attested audit trail. The token_id is stored with the facts + // so anyone can verify who/what wrote them and under what scope. + // + // Flow: + // 1. Agent requests PTP token: POST /api/v1/ptp/token + // scope: "corpus:write:personaforge:{slug}" (or "corpus:write:personaforge:*") + // purpose: "QA review and verified fact injection for [Subject Name]" + // 2. Agent presents token here with facts payload + // 3. Server verifies token validity + scope coverage + // 4. Facts stored with ptp_token_id, attested_at, vcap_attestation_uri + // 5. Returns verifiable attestation record + app.post("/api/steward/:slug/bio-facts/ptp", async (req: Request, res: Response) => { + const { slug } = req.params; + const { token_id, bio_facts_md, wiki_summary_md, session_notes } = req.body; + if (!token_id) return res.status(400).json({ error: "token_id required (PTP presence token)" }); + if (bio_facts_md === undefined && wiki_summary_md === undefined) { + return res.status(400).json({ error: "At least one of bio_facts_md or wiki_summary_md is required" }); + } + + try { + // Verify the PTP token and check scope coverage for this subject + const requiredScope = `corpus:write:personaforge:${slug}`; + const ptpResult = await pool.query(`SELECT * FROM ptp_tokens WHERE id = $1`, [token_id]); + if (!ptpResult.rows.length) return res.status(403).json({ error: "ptp_token_not_found", token_id }); + + const tk = ptpResult.rows[0]; + const now = new Date(); + if (tk.revoked) return res.status(403).json({ error: "ptp_token_revoked", revoked_at: tk.revoked_at }); + if (tk.expires_at && new Date(tk.expires_at) < now) { + return res.status(403).json({ error: "ptp_token_expired", expired_at: tk.expires_at }); + } + + // Scope check: token scope must equal or be a wildcard superset of corpus:write:personaforge:{slug} + const tokenParts = (tk.scope as string).split(":"); + const requiredParts = requiredScope.split(":"); + let scopeOk = true; + for (let i = 0; i < requiredParts.length; i++) { + if (tokenParts[i] !== "*" && tokenParts[i] !== requiredParts[i]) { scopeOk = false; break; } + } + if (!scopeOk) { + return res.status(403).json({ + error: "scope_insufficient", + token_scope: tk.scope, + required_scope: requiredScope, + hint: `Request a token with scope "corpus:write:personaforge:${slug}" or "corpus:write:personaforge:*"`, + }); + } + + // Ensure subject row exists + await pool.query( + `INSERT INTO personaforge_subjects (slug, subject_name, build_status) + VALUES ($1, $1, 'registered') ON CONFLICT (slug) DO NOTHING`, [slug] + ); + + // Write facts + stamp with PTP attestation metadata + const attestedAt = new Date().toISOString(); + if (bio_facts_md !== undefined) { + await pool.query( + `UPDATE personaforge_subjects + SET bio_facts_md = $2, + bio_facts_ptp_token_id = $3, + bio_facts_attested_at = $4, + bio_facts_vcap_uri = $5, + updated_at = NOW() + WHERE slug = $1`, + [slug, bio_facts_md, token_id, attestedAt, tk.vcap_attestation_uri] + ); + } + if (wiki_summary_md !== undefined) { + await pool.query( + `UPDATE personaforge_subjects + SET wiki_summary_md = $2, + wiki_ptp_token_id = $3, + wiki_attested_at = $4, + updated_at = NOW() + WHERE slug = $1`, + [slug, wiki_summary_md, token_id, attestedAt] + ); + } + + // Log to corpus_consult_log if it exists (non-fatal) + await pool.query( + `INSERT INTO corpus_consult_log + (slug, event_type, token_id, scope, purpose, attested_at, session_notes, created_at) + VALUES ($1, 'ptp_bio_injection', $2, $3, $4, $5, $6, NOW())`, + [slug, token_id, tk.scope, tk.purpose_declaration, attestedAt, session_notes || null] + ).catch(() => {}); // table may not exist in all environments + + res.json({ + ok: true, + slug, + attested_at: attestedAt, + token_id, + scope: tk.scope, + vcap_attestation_uri: tk.vcac_attestation_uri || tk.vcap_attestation_uri, + fields_written: [ + ...(bio_facts_md !== undefined ? ["bio_facts_md"] : []), + ...(wiki_summary_md !== undefined ? ["wiki_summary_md"] : []), + ], + verification: `GET https://wellspr.ing/api/v1/ptp/token/${token_id}`, + note: "Biographical facts are now stamped with this PTP token. The corpus build will inject them as priority-0 chunks.", + }); + } catch (e: any) { + console.error("[StewardDash] PTP injection error:", e.message); + res.status(500).json({ error: e.message }); + } + }); + + // ── GET /api/steward/:slug/bio-facts ──────────────────────────────────── + app.get("/api/steward/:slug/bio-facts", async (req: Request, res: Response) => { + const { slug } = req.params; + const token = req.query.token as string; + if (!await validateStewardToken(slug, token)) return res.status(403).json({ error: "Invalid token" }); + try { + const { rows } = await pool.query( + `SELECT bio_facts_md, wiki_summary_md FROM personaforge_subjects WHERE slug = $1`, [slug] + ); + const row = rows[0] || {}; + res.json({ bio_facts_md: row.bio_facts_md || null, wiki_summary_md: row.wiki_summary_md || null }); + } catch (e: any) { + res.status(500).json({ error: e.message }); + } + }); + + // ── POST /api/steward/:slug/bio-facts ──────────────────────────────────── + // Requires steward token (or admin key). Updates bio_facts_md and/or wiki_summary_md. + app.post("/api/steward/:slug/bio-facts", async (req: Request, res: Response) => { + const { slug } = req.params; + const { token, bio_facts_md, wiki_summary_md } = req.body; + const adminKey = (req.headers["x-admin-key"] as string) || req.query.admin_key as string; + const isAdmin = adminKey === ADMIN_KEY; + if (!isAdmin && !await validateStewardToken(slug, token)) return res.status(403).json({ error: "Invalid token or admin key" }); + + if (bio_facts_md === undefined && wiki_summary_md === undefined) return res.status(400).json({ error: "Nothing to update" }); + + try { + // Ensure the subject row exists (auto-create if missing, e.g. dev DB or fresh environment) + await pool.query( + `INSERT INTO personaforge_subjects (slug, subject_name, build_status) + VALUES ($1, $1, 'registered') + ON CONFLICT (slug) DO NOTHING`, + [slug] + ); + + // Update whichever fields were provided + if (bio_facts_md !== undefined) { + await pool.query( + `UPDATE personaforge_subjects SET bio_facts_md = $2, updated_at = NOW() WHERE slug = $1`, + [slug, bio_facts_md] + ); + } + if (wiki_summary_md !== undefined) { + await pool.query( + `UPDATE personaforge_subjects SET wiki_summary_md = $2, updated_at = NOW() WHERE slug = $1`, + [slug, wiki_summary_md] + ); + } + res.json({ ok: true }); + } catch (e: any) { + res.status(500).json({ error: e.message }); + } + }); + + // ── POST /api/steward/:slug/artifacts/inject ───────────────────────────── + // Admin-only. Inject a manual artifact with full text content (no URL required). + // Used for Wikipedia excerpts, articles-about-the-person, curated secondary sources. + app.post("/api/steward/:slug/artifacts/inject", async (req: Request, res: Response) => { + const { slug } = req.params; + const adminKey = (req.headers["x-admin-key"] as string) || req.body.admin_key; + if (adminKey !== ADMIN_KEY) return res.status(403).json({ error: "Admin key required" }); + const { title, content_text, artifact_type, source_url, pub_date } = req.body; + if (!title || !content_text) return res.status(400).json({ error: "title and content_text required" }); + const artifactId = `${slug}::injected::${crypto.randomUUID().slice(0, 8)}`; + try { + await pool.query( + `INSERT INTO personaforge_artifacts + (subject_slug, artifact_id, title, source_url, artifact_type, content_text, curation_status, pub_date) + VALUES ($1, $2, $3, $4, $5, $6, 'approved', $7) + ON CONFLICT (artifact_id) DO UPDATE SET content_text = EXCLUDED.content_text, title = EXCLUDED.title`, + [slug, artifactId, title, source_url || null, artifact_type || 'injected', content_text, pub_date || null] + ); + await pool.query(`UPDATE personaforge_subjects SET updated_at = NOW() WHERE slug = $1`, [slug]); + res.json({ ok: true, artifact_id: artifactId }); + } catch (e: any) { + res.status(500).json({ error: e.message }); + } + }); + + // ── GET /api/steward/:slug/dashboard-token (admin only) ───────────────── + app.get("/api/steward/:slug/dashboard-token", async (req: Request, res: Response) => { + const { slug } = req.params; + const key = (req.headers["x-admin-key"] as string) || req.query.admin_key as string; + if (key !== ADMIN_KEY) return res.status(403).json({ error: "Admin key required" }); + + const token = await getOrCreateDashboardToken(slug); + const dashUrl = `https://agentify.help/steward/${slug}?token=${token}`; + res.json({ slug, dashboard_token: token, dashboard_url: dashUrl }); + }); +} + +// ── Approved-only corpus build (async) ─────────────────────────────────────── +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; + +const S3_HOSTNAME = process.env.VULTR_S3_HOSTNAME || "ewr1.vultrobjects.com"; +const S3_BUCKET = process.env.CORPUS_S3_BUCKET || "agentify-corpus"; + +function getS3(): S3Client { + return new S3Client({ + endpoint: `https://${S3_HOSTNAME}`, + region: "ewr1", + forcePathStyle: true, + credentials: { + accessKeyId: process.env.VULTR_S3_ACCESS_KEY || "", + secretAccessKey: process.env.VULTR_S3_SECRET_KEY || "", + }, + }); +} + +function chunkText(text: string, maxWords = 400): string[] { + const sentences = text.split(/(?<=[.!?])\s+/); + const chunks: string[] = []; + let current = ""; + for (const s of sentences) { + const proposed = current ? current + " " + s : s; + if (proposed.split(/\s+/).length > maxWords && current) { + chunks.push(current.trim()); + current = s; + } else { + current = proposed; + } + } + if (current.trim()) chunks.push(current.trim()); + return chunks.filter(c => c.split(/\s+/).length >= 30); +} + +async function broadcastStewardLog(slug: string, msg: string) { + console.log(`[StewardBuild/${slug}] ${msg}`); + await pool.query( + `UPDATE personaforge_subjects SET build_log = COALESCE(build_log,'') || $2, updated_at=NOW() WHERE slug=$1`, + [slug, `[${new Date().toISOString().slice(11, 19)}] ${msg}\n`] + ).catch(() => {}); +} + +async function runApprovedBuild(slug: string, bundleId: string, approvedIds: string[]) { + const s3 = getS3(); + const allChunks: any[] = []; + let chunkSeq = 0; + let processed = 0; + let errors = 0; + + const { rows: pf } = await pool.query( + `SELECT subject_name, domain, corpus_version, bio_facts_md, wiki_summary_md FROM personaforge_subjects WHERE slug = $1`, [slug] + ); + const subject = pf[0] || { subject_name: slug, domain: "", corpus_version: "1.0.0" }; + + await broadcastStewardLog(slug, `Starting approved-only build: ${approvedIds.length} artifacts, bundle ${bundleId}`); + + // ── Priority chunk 0: Biographical baseline facts ────────────────────── + if (subject.bio_facts_md && subject.bio_facts_md.trim()) { + const bioChunks = chunkText(subject.bio_facts_md); + for (const chunk of bioChunks) { + allChunks.push({ + chunk_id: `${slug}-${bundleId}-bio-${String(chunkSeq++).padStart(5, "0")}`, + subject_slug: slug, + artifact_id: `${slug}::bio_facts_baseline`, + title: "Biographical Baseline — Ground-Truth Facts", + pub_date: null, + artifact_type: "bio_facts", + priority: "critical", + text: chunk, + }); + } + await broadcastStewardLog(slug, `Injected biographical baseline (${bioChunks.length} chunks) as priority-0 corpus anchor.`); + } + + // ── Priority chunk 1: Wikipedia / secondary sources ──────────────────── + if (subject.wiki_summary_md && subject.wiki_summary_md.trim()) { + const wikiPreamble = `[SKEPTICISM NOTE: The following is from Wikipedia and secondary sources. Treat as background reference, not first-person memory. Verify contested facts against primary corpus.]\n\n`; + const wikiChunks = chunkText(wikiPreamble + subject.wiki_summary_md); + for (const chunk of wikiChunks) { + allChunks.push({ + chunk_id: `${slug}-${bundleId}-wiki-${String(chunkSeq++).padStart(5, "0")}`, + subject_slug: slug, + artifact_id: `${slug}::wiki_secondary_sources`, + title: "Wikipedia & Secondary Sources — Skepticism-Weighted", + pub_date: null, + artifact_type: "wiki_secondary", + priority: "background", + text: chunk, + }); + } + await broadcastStewardLog(slug, `Injected Wikipedia/secondary sources (${wikiChunks.length} chunks) with skepticism weighting.`); + } + + for (const artifactId of approvedIds) { + const { rows: arts } = await pool.query( + `SELECT title, source_url, pub_date, artifact_type, content_text FROM personaforge_artifacts WHERE artifact_id = $1`, [artifactId] + ); + if (!arts[0]) continue; + const art = arts[0]; + + try { + await broadcastStewardLog(slug, `Fetching: ${art.title}`); + // Use inline content_text if present (injected artifacts), otherwise fetch from URL + const text = art.content_text || (art.source_url ? await fetchEssayTextBuild(art.source_url) : ""); + if (!text || text.split(/\s+/).length < 50) { + await broadcastStewardLog(slug, `Skipped (too short): ${art.title}`); + errors++; + continue; + } + const chunks = chunkText(text); + const wc = text.split(/\s+/).length; + for (const chunk of chunks) { + allChunks.push({ + chunk_id: `${slug}-${bundleId}-${String(chunkSeq++).padStart(5, "0")}`, + subject_slug: slug, + artifact_id: artifactId, + title: art.title, + pub_date: art.pub_date || null, + artifact_type: art.artifact_type || "essay", + text: chunk, + }); + } + await pool.query( + `UPDATE personaforge_artifacts SET chunk_count = $1, word_count = $2 WHERE artifact_id = $3`, + [chunks.length, wc, artifactId] + ); + processed++; + } catch (e: any) { + await broadcastStewardLog(slug, `Error on ${art.title}: ${e.message}`); + errors++; + } + } + + await broadcastStewardLog(slug, `All artifacts processed. ${allChunks.length} chunks total. Uploading to S3…`); + + try { + const prefix = `staging/${slug}/v${subject.corpus_version}/${bundleId}`; + const ndjson = allChunks.map(c => JSON.stringify(c)).join("\n"); + await s3.send(new PutObjectCommand({ + Bucket: S3_BUCKET, + Key: `${prefix}/chunks/index.ndjson`, + Body: ndjson, + ContentType: "application/x-ndjson", + })); + + const manifest = { + bundle_id: bundleId, + subject_slug: slug, + subject_name: subject.subject_name, + domain: subject.domain, + corpus_version: subject.corpus_version, + chunk_count: allChunks.length, + artifact_count: processed, + approved_only: true, + built_at: new Date().toISOString(), + }; + await s3.send(new PutObjectCommand({ + Bucket: S3_BUCKET, + Key: `${prefix}/manifest.json`, + Body: JSON.stringify(manifest, null, 2), + ContentType: "application/json", + })); + + await pool.query( + `UPDATE personaforge_subjects SET build_status = 'built', chunk_count = $2, updated_at = NOW() WHERE slug = $1`, + [slug, allChunks.length] + ); + await broadcastStewardLog(slug, `Build complete. ${allChunks.length} chunks uploaded. Bundle: ${bundleId}`); + if (errors > 0) await broadcastStewardLog(slug, `Note: ${errors} artifacts had errors and were skipped.`); + } catch (e: any) { + await broadcastStewardLog(slug, `S3 upload error: ${e.message}`); + await pool.query(`UPDATE personaforge_subjects SET build_status = 'error' WHERE slug = $1`, [slug]); + } +} + +async function fetchEssayTextBuild(url: string): Promise { + try { + const html = await fetch(url, { + headers: { "User-Agent": "WellSpr.ing PersonaForge/1.0" }, + signal: AbortSignal.timeout(20000), + }).then(r => r.text()); + return html + .replace(/]*>[\s\S]*?<\/script>/gi, " ") + .replace(/]*>[\s\S]*?<\/style>/gi, " ") + .replace(/<[^>]+>/g, " ") + .replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, " ") + .replace(/\s+/g, " ").trim(); + } catch { + return ""; + } +}