Skip to main content

Governor

Policy, budgets, and outbound decisions. Governor is the subsystem that lets an agent reason about should I, not just can I.

Tables

TablePurpose
governor.policiesNamed rule sets (allow-lists, spend caps, tool gates).
governor.decisionsAppend-only log of (agent_id, policy, action, verdict, reason).
governor.budgetsPer-agent per-window spend counters.

Example policies

  • Domain allow-list. Outbound model calls must target a domain in governor.policies(key='model_domains'). Everything else returns verdict='deny' with reason='domain_not_allowed'.
  • Daily spend cap. governor.check_budget(agent_id, cents) rejects once the rolling window is exhausted.
  • Tool gate. Dangerous tools (e.g. code execution) require an explicit policy row per agent.

Routes

MethodPathPurpose
POST/policy/putCreate or replace a policy.
POST/policy/listEnumerate an agent's policies.
POST/checkAsk for a verdict on a proposed action.
POST/budget/chargeDebit a budget after a successful action.
POST/decisions/listRead the decisions log.

Quotas and billing

Quota enforcement lives in the same layer. Every metered callable wraps a Firestore transaction via requireQuota(ownerUid, agentId, action, units) that atomically (a) reads the owner's current plan from subscriptions/{ownerUid}, (b) reads the month-to-date counter at agents/{agentId}/usage/{YYYY-MM}, and (c) increments or rejects with resource-exhausted. Because check and debit share the transaction, there is no read-then-write race.

Counters are credited back (credit(agentId, action, units)) when the surrounding operation throws after the debit — so a Firestore write that fails doesn't count against the user's quota.

Plans map to per-action limits in functions/src/lib/plans.ts:

ActionFreeBuilderScale
postbox.send100/mo100,000/mo
mesh.issueCert50/mo50,000/mo
scheduler.createJob5 live500 live
artifacts.grant25/mo25,000/mo
tools.invoke100/mo100,000/mo

The subscriptions/{ownerUid} doc is hydrated by the RevenueCat webhook (functions/src/billing/webhook.ts). ACTIVE_EVENTS toggle proActive; expiresAtMs is honored so a lapsed subscription quietly drops back to the free plan on the next quota check without requiring a re-webhook.

Why "decisions" is its own log

Governor writes audit.events rows like everything else, but it also keeps its own high-volume governor.decisions table for cheap, indexed reads. Audit stays authoritative for security; decisions stay fast for dashboards.