functions/index.jstoday → governed
// TODAY — a hard-coded constant; flat "operator or not"
const OPERATOR_EMAILS = [
'antano@antanoharini.com', 'solar345@gmail.com', 'solar345@yahoo.com', 'harini@antanoharini.com' ];
// every operator endpoint repeats this gate:
if (!OPERATOR_EMAILS.includes(user.email))
return res.status(403)...
// GOVERNED — config/roles AUGMENTS the floor
const role = await roleOf(db, email); // config/roles ∪ floor
canOperate(role) // Bench: operator|optimizer|pioneer|admin
canGovern(role) // Desk : pioneer|admin → else 403
canAdmin(role) // the allowlist write only
config/roles · liveFirestore doc
// reading config/roles…
Today access is a flat constant — OPERATOR_EMAILS on index.js — and the gate (OPERATOR_EMAILS.includes(user.email)) is copy-pasted across optimize, optimizeRuns, personas, cost, and metrics. It only answers a yes/no question: operator, or not?
roleOf(db,email) augments that constant: it reads config/roles, returns the role string, and is floored so the legacy emails always keep Bench access — the floor can elevate, never demote.7 Bench endpoints accept operator · optimizer · pioneer · admin; the Desk endpoints — the cross-operator board, A/B, and the lock — require pioneer (canGovern); writing the allowlist itself requires admin (canAdmin).
request · user.email
→
roleOf()
→
config/roles ∪ floor
→
pioneer · operator · optimizer · admin · ∅
The migration is faithful: the four emails in today's OPERATOR_EMAILS all keep Bench access (the operator floor). Two of them — Antano & Harini — are the bootstrap pioneers, which is what unlocks this console. Nothing that worked stops working; the map just gains a vocabulary.
Zero-variation
The same
roleOf() is the one guard the engine, the optimizer, and this console all call — one code path, never re-implemented. A role change takes effect within the config cache TTL (~30s),
no deploy.
5