feat: co-steward invite/list/remove endpoints, co_steward_emails schema

This commit is contained in:
gitadmin 2026-05-02 14:13:14 +00:00
parent abefc012fa
commit 0bfdb2285e

View file

@ -61,6 +61,9 @@ export async function initStewardDash() {
`); `);
// Also add to registry for forward-compat (new stewards via web form) // 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(() => {}); await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS dashboard_token TEXT`).catch(() => {});
// Co-steward collaboration: array of invited co-steward emails
await pool.query(`ALTER TABLE agentify_subject_registry ADD COLUMN IF NOT EXISTS co_steward_emails TEXT[] DEFAULT '{}'`).catch(() => {});
await pool.query(`ALTER TABLE personaforge_subjects ADD COLUMN IF NOT EXISTS co_steward_emails TEXT[] DEFAULT '{}'`).catch(() => {});
console.log("[StewardDash] Schema columns ready"); console.log("[StewardDash] Schema columns ready");
} }
@ -1248,6 +1251,80 @@ Then give your verdict on a new line: "Verdict: Approve", "Verdict: Flag", or "V
const dashUrl = `https://agentify.help/steward/${slug}?token=${token}`; const dashUrl = `https://agentify.help/steward/${slug}?token=${token}`;
res.json({ slug, dashboard_token: token, dashboard_url: dashUrl }); res.json({ slug, dashboard_token: token, dashboard_url: dashUrl });
}); });
// ── POST /api/steward/:slug/invite-co-steward ────────────────────────────
// Invite a collaborator email to co-steward this agent.
// They gain access via the /my-agents portfolio magic-link flow.
app.post("/api/steward/:slug/invite-co-steward", express.json({ limit: "1mb" }), async (req: Request, res: Response) => {
if (!isAgentify(req)) return res.status(404).json({ error: "Not found" });
const { slug } = req.params;
const token = req.query.token as string | undefined;
const key = (req.headers["x-admin-key"] as string) || (req.query.admin_key as string);
if (key !== ADMIN_KEY && !(await validateStewardToken(slug, token))) {
return res.status(403).json({ error: "Steward token or admin key required" });
}
const { email } = req.body ?? {};
if (!email || !email.includes("@")) return res.status(400).json({ error: "Valid email required" });
const normalized = email.toLowerCase().trim();
await pool.query(
`UPDATE agentify_subject_registry
SET co_steward_emails = array_append(COALESCE(co_steward_emails, '{}'), $2)
WHERE subject_slug = $1
AND NOT ($2 = ANY(COALESCE(co_steward_emails, '{}')))`,
[slug, normalized]
).catch(() => {});
await pool.query(
`UPDATE personaforge_subjects
SET co_steward_emails = array_append(COALESCE(co_steward_emails, '{}'), $2)
WHERE slug = $1
AND NOT ($2 = ANY(COALESCE(co_steward_emails, '{}')))`,
[slug, normalized]
).catch(() => {});
res.json({ ok: true, slug, co_steward_email: normalized,
note: "Co-steward added. They can now use /my-agents with their email to receive a portal access link." });
});
// ── GET /api/steward/:slug/co-stewards ───────────────────────────────────
app.get("/api/steward/:slug/co-stewards", async (req: Request, res: Response) => {
if (!isAgentify(req)) return res.status(404).json({ error: "Not found" });
const { slug } = req.params;
const token = req.query.token as string | undefined;
const key = (req.headers["x-admin-key"] as string) || (req.query.admin_key as string);
if (key !== ADMIN_KEY && !(await validateStewardToken(slug, token))) {
return res.status(403).json({ error: "Steward token or admin key required" });
}
const { rows } = await pool.query(
`SELECT co_steward_emails FROM agentify_subject_registry WHERE subject_slug = $1`, [slug]
).catch(() => ({ rows: [] }));
res.json({ slug, co_steward_emails: rows[0]?.co_steward_emails || [] });
});
// ── DELETE /api/steward/:slug/co-stewards/:email ─────────────────────────
app.delete("/api/steward/:slug/co-stewards/:email", async (req: Request, res: Response) => {
if (!isAgentify(req)) return res.status(404).json({ error: "Not found" });
const { slug, email } = req.params;
const token = req.query.token as string | undefined;
const key = (req.headers["x-admin-key"] as string) || (req.query.admin_key as string);
if (key !== ADMIN_KEY && !(await validateStewardToken(slug, token))) {
return res.status(403).json({ error: "Steward token or admin key required" });
}
const normalized = decodeURIComponent(email).toLowerCase().trim();
await pool.query(
`UPDATE agentify_subject_registry
SET co_steward_emails = array_remove(COALESCE(co_steward_emails, '{}'), $2)
WHERE subject_slug = $1`,
[slug, normalized]
).catch(() => {});
await pool.query(
`UPDATE personaforge_subjects
SET co_steward_emails = array_remove(COALESCE(co_steward_emails, '{}'), $2)
WHERE slug = $1`,
[slug, normalized]
).catch(() => {});
res.json({ ok: true, slug, removed: normalized });
});
} }
// ── Approved-only corpus build (async) ─────────────────────────────────────── // ── Approved-only corpus build (async) ───────────────────────────────────────