# RBAC — Access Control for Agent Fleets

Powerloom uses an Active Directory-style access model to govern who can do what across your agent fleet. Organizational units, security groups, role bindings, and deny-override — the same patterns IT admins use for users, applied to agents and their tools.

---

## The Model

Four concepts. Every access decision in your organization resolves to these.

### Organizational Units

OUs are the hierarchy. They mirror your org chart — by team, environment, or tenant.

```
acme/
├── engineering/
│   ├── platform/       ← 2 agents, 2 MCP servers
│   └── support/
└── accounting/
```

Every resource — agents, MCP servers, skills, credentials — lives inside an OU. Policies at a parent OU flow down to children. A binding at `acme/` applies to everything underneath it.

One root OU per organization. Unlimited nesting below that.

### Groups

Groups collect principals. A principal is anything that can hold permissions — a user, another group, or an OU.

Groups support nesting. Add a group to a group, and the members of the inner group inherit the outer group's bindings transitively. Powerloom maintains a closure table to resolve transitive membership in constant time, so nested groups don't slow down permission checks regardless of depth.

Cycle detection is enforced at the database level. You can't create circular group memberships.

### Roles

A role is a named set of permissions. Powerloom ships five built-in roles:

| Role | Purpose |
|---|---|
| **OrgAdmin** | Full access to everything in the organization |
| **OUAdmin** | Full access within a specific OU subtree |
| **AgentBuilder** | Create and configure agents and skills. Read-only on org structure. |
| **AgentOperator** | Invoke agents. No creation or modification rights. |
| **AgentViewer** | Read-only across agents, skills, and MCP servers |

Permissions use a `resource:action` format: `agent:create`, `skill:read`, `mcp:register`, `binding:delete`. Roles bundle these into meaningful job functions.

### Role Bindings

A binding connects a principal to a role at a specific OU scope, with a decision type of **allow** or **deny**.

```yaml
- principal: group:eng-leads
  role: OUAdmin
  scope: /acme/engineering
  effect: allow

- principal: group:contractors
  role: AgentBuilder
  scope: /acme
  effect: deny
```

The first binding grants `eng-leads` full admin rights on everything under `/acme/engineering` and its descendants. The second denies `contractors` the ability to build agents anywhere in the organization.

---

## How Permissions Are Evaluated

When a user takes an action — creating an agent, invoking a session, attaching a skill — Powerloom evaluates whether they're allowed. The algorithm:

**1. Collect the user's effective principals.**

This includes:
- The user's own identity
- Every OU from the user's home OU up to the root (OU principals)
- Every group the user belongs to, including transitively nested groups

**2. Find all matching bindings.**

A binding matches if:
- Its principal is one of the user's effective principals
- Its role includes the required permission
- Its scope OU is an ancestor of (or equal to) the resource's OU

The OU closure table makes this a single indexed query — no tree traversal at request time.

**3. Apply deny-override.**

- If **any** matching binding is `deny` → access denied
- Else if **any** matching binding is `allow` → access granted
- Else → access denied (implicit default deny)

One deny anywhere in the matching set blocks the permission. This lets an admin revoke access with a single binding without touching existing allow bindings.

**4. Cache the result.**

Permission checks are memoized per request. The same user hitting the same permission on the same OU within one request resolves once.

---

## Deny-Override in Practice

Deny always wins. This is the same semantics as Active Directory's deny precedence.

**Scenario:** Bob has two bindings on `/acme`:
- Allow: `AgentOperator` (grants `agent:invoke`)
- Deny: `AgentOperator` (denies `agent:invoke`)

Result: denied. The deny binding overrides the allow, regardless of which was created first.

This is useful for broad-allow-with-exceptions patterns:

```
Allow: eng-leads → OUAdmin on /acme/engineering     (broad access)
Deny:  contractors → AgentBuilder on /acme           (carved-out exception)
```

Every engineer in `eng-leads` gets full OU admin rights. But any engineer who's also in the `contractors` group is denied agent-building rights, even though their `eng-leads` membership would otherwise allow it.

---

## Inheritance

Permissions flow in two directions:

**Down the OU tree.** A binding at `/acme` applies to `/acme/engineering`, `/acme/engineering/platform`, and every other descendant. You don't need to repeat bindings at every level.

**Through group nesting.** If `sales-team` contains `managers`, and `managers` contains Alice, then a binding on `sales-team` applies to Alice. The closure table resolves this transitively — it doesn't matter how many layers of nesting exist.

These combine naturally. A binding granting `OUAdmin` to group `eng-leads` at scope `/acme/engineering` gives every member of `eng-leads` — including transitive members from nested groups — full admin rights on every resource in every descendant OU of `/acme/engineering`.

---

## Skill Access Grants

Skills have an additional access mechanism beyond standard RBAC. A skill can be granted to specific principals directly, independent of OU-scoped role bindings.

This covers the case where a skill lives in one OU but needs to be accessible by users or agents in another. A direct grant bypasses the OU scope check — the principal can access the skill regardless of where it sits in the hierarchy.

Grants are additive. Standard RBAC still applies in parallel. If a user has `skill:read` on the skill's OU via a role binding, the grant is redundant.

---

## Safety Guarantees

**Last-admin protection.** You can't delete the sole `OrgAdmin` binding at the organization root. Powerloom prevents you from locking yourself out.

**Implicit default deny.** If no binding matches, the answer is no. Permissions are opt-in, never opt-out.

**Closure table integrity.** OU and group hierarchies are maintained by database triggers. Application code can't corrupt the transitive relationships — they're recomputed automatically on every structural change.

**No cross-org leakage.** Every binding includes an `organization_id`. Bindings, roles, and permissions are scoped to a single organization. There is no mechanism for one organization's RBAC to affect another.

---

## API Reference

### Organizational Units

| Method | Endpoint | Description |
|---|---|---|
| GET | `/ous` | List all OUs |
| GET | `/ous/tree` | Full hierarchy as a nested tree |
| GET | `/ous/{ou_id}` | Get a single OU |
| POST | `/ous` | Create an OU |
| PATCH | `/ous/{ou_id}` | Update an OU (rename, reparent) |
| DELETE | `/ous/{ou_id}` | Delete an OU (must have no children or groups) |

### Groups

| Method | Endpoint | Description |
|---|---|---|
| GET | `/groups` | List groups (filter by `ou_id`) |
| GET | `/groups/{group_id}` | Get group with direct members |
| POST | `/groups` | Create a group |
| PATCH | `/groups/{group_id}` | Update a group |
| DELETE | `/groups/{group_id}` | Delete a group (cascades memberships) |
| POST | `/groups/{group_id}/users` | Add a user to the group |
| DELETE | `/groups/{group_id}/users/{user_id}` | Remove a user |
| POST | `/groups/{group_id}/groups` | Nest a group inside this group |
| DELETE | `/groups/{group_id}/groups/{member_group_id}` | Remove a nested group |

### Role Bindings

| Method | Endpoint | Description |
|---|---|---|
| GET | `/role-bindings` | List all bindings in the organization |
| POST | `/role-bindings` | Create a binding (allow or deny) |
| DELETE | `/role-bindings/{binding_id}` | Delete a binding |
| GET | `/roles` | List all available roles |
