AI Agent Governance
Every AI agent on a RootCX Core is a governed principal. It has its own identity, its own role, and its own audit trail. It acts under delegation from a human, and its authority is bounded on both sides at once. No configuration is required. The governance is structural.
Default authority: least-privilege
On first deploy, the Core gives the agent a role named app:{appId}:agent, derived from the app's data contract: create, read, update, and delete on the app's own entities, plus the permission to be invoked. Not admin, not a wildcard. The agent can work with its own app's data out of the box and nothing more.
The Core assigns this role only on the first deploy. After that, the role is yours to govern through the roles API. Redeploying the agent never overwrites a role an admin has changed.
Authority model
The Core computes effective authority on every action:
effective = agent_role_permissions ∩ delegator_permissions
The agent can never exceed its assigned role. The agent can never exceed the human who triggered it. Both boundaries apply at once.
| Agent role | Delegator perms | Effective authority |
|---|---|---|
["app:crm:contacts.read"] |
["*"] (admin) |
["app:crm:contacts.read"] |
["app:crm:*"] |
["app:crm:contacts.read"] |
["app:crm:contacts.read"] |
["*"] (unrestricted) |
["app:crm:*"] |
["app:crm:*"] |
| any | [] (offboarded user) |
[] (deny all) |
The intersection is computed server-side on every tool call and data mutation. It is not cached across requests: change a role, and it applies on the next action.
Restricting an agent
To narrow an agent below its default, use the standard roles API, no redeploy needed:
# 1. Create a narrow role
POST /api/v1/roles
{ "name": "invoice-reader", "permissions": ["app:billing:invoices.read", "tool:query_data"] }
# 2. Revoke the agent's default role
POST /api/v1/roles/revoke
{ "userId": "<agent-uuid>", "role": "app:billing:agent" }
# 3. Assign the narrow role
POST /api/v1/roles/assign
{ "userId": "<agent-uuid>", "role": "invoice-reader" }
The change takes effect on the agent's next action. Its running worker is invalidated, so there is no stale authority to wait out.
Delegation
Every time an agent acts, it acts under delegation from a human. See Delegation for the shared model behind agents and service accounts.
Interactive invocation. A user clicks "run" or sends a message. Their identity is the delegator, and the intersection applies immediately.
Autonomous triggers. Cron jobs, entity hooks, and webhooks fire with no human present. Each one carries a standing mandate: the identity of the human who created it, recorded as a delegation.
Standing mandates
Before an autonomous trigger dispatches its agent, the Core runs a fire-time gate. All 4 checks must pass:
- The trigger has an owner.
- The owner is still enabled (not disabled or offboarded).
- A valid delegation grant exists (not revoked, not expired).
- The owner still holds
app:{appId}:invoke.
If any check fails, the agent does not fire. An offboarded owner is a dead trigger.
Deny-by-default
An agent has zero authority without explicit human authorization:
- No invoker (trigger has no owner) → deny.
- No delegation (mandate revoked or missing) → deny.
- Owner offboarded (disabled, or permissions revoked) → deny.
- Permission outside the intersection → deny.
This is enforced at the scheduler before any agent code runs, and again at the data layer on every action a running agent attempts.
Invocation ACL
Invoking an agent requires the app:{appId}:invoke permission. The key is auto-generated when the app is installed and can be granted to any role. Only principals that hold it (or a global admin) can trigger the agent.
No tokens to forge
The agent worker never receives a token, and there is no delegated credential it could replay. Authority is bound to the worker process out-of-band: each principal gets its own worker, and the Core poses the identity to PostgreSQL as transaction-local settings the worker cannot rewrite. Permissions are resolved live from the database on every check, so revoking a role takes effect immediately. There is nothing to expire.
See Enforcement → Worker isolation.
Audit trail
Every data mutation captures dual-identity attribution:
| Field | Description |
|---|---|
actor_uid |
Who performed the action (the agent's UUID) |
delegator_uid |
On whose authority (the human's UUID; NULL for direct user actions) |
trigger_ref |
How it was triggered (agent_tool, api, app_sql, and similar) |
During an incident this answers 3 questions at once: who acted, on whose authority, and through what mechanism. See Audit Log.
Security properties
| Property | How it is enforced |
|---|---|
| Agent cannot exceed its role | Intersection on every tool call and mutation |
| Agent cannot exceed the delegator | Intersection on every tool call and mutation |
| No human, no action | Fire-time gate at the scheduler + RLS at the data layer |
| Offboarded owner kills the agent | Standing mandate revalidated at dispatch |
| Every action is attributable | Dual-identity audit at the PostgreSQL trigger level |
| Agent cannot impersonate a user | 1 worker process per identity; identity posed by the Core, not the worker |
| Agent cannot disable its own checks | Authorization runs in the Core and in PostgreSQL, outside the agent process |
Compliance
| Standard | How RootCX addresses it |
|---|---|
| NIST SP 800-207 (Zero Trust) | Never trust, always verify. Authority recomputed on every action; no implicit trust carried from a prior action. |
| SOC 2 CC6.1 / CC6.3 | Per-principal logical access via the roles API; an immutable audit trail on every mutation. |
| EU AI Act Article 14 | Human oversight by design. Every agent action traces back to an accountable human. |
Next steps
- RBAC: creating, assigning, and revoking roles.
- Enforcement: the data path and worker isolation.
- Audit Log: querying the dual-identity trail.
- Scheduled Jobs and Webhooks: autonomous triggers.