Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@
"docs/features/context-files",
"docs/features/gateway",
"docs/features/gateway-hot-reload",
"docs/features/gateway-operator-scopes",
"docs/features/gateway-overview",
"docs/features/gateway-session-persistence",
"docs/features/gateway-session-continuity",
Expand Down
3 changes: 3 additions & 0 deletions docs/features/gateway-bind-aware-auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ The `PRAISONAI_ALLOW_DEFAULT_CREDS=1` escape hatch should only be used for ephem
## Related

<CardGroup cols={2}>
<Card title="Operator Scopes" icon="shield-check" href="/docs/features/gateway-operator-scopes">
Least-privilege multi-operator access control
</Card>
<Card title="Gateway Documentation" icon="gateway" href="/docs/gateway">
Core gateway functionality and configuration
</Card>
Expand Down
308 changes: 308 additions & 0 deletions docs/features/gateway-operator-scopes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
---
title: "Gateway Operator Scopes"
sidebarTitle: "Operator Scopes"
description: "Least-privilege role-based access control for multi-operator Gateway deployments"
icon: "shield-check"
---

Operator scopes grant teammates least-privilege access to a shared Gateway — read-only dashboards, send-but-not-approve operators, or full admins — without handing over the whole keys.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The phrase "without handing over the whole keys" is grammatically awkward. Consider changing it to "without handing over the master keys" or "without handing over all the keys" for better clarity and professional tone.

Operator scopes grant teammates least-privilege access to a shared Gateway — read-only dashboards, send-but-not-approve operators, or full admins — without handing over the master keys.


```mermaid
graph LR
subgraph "Multi-Operator Gateway"
V[👁 Viewer<br/>read] --> GW[🌐 Gateway]
O[✉️ Ops<br/>read + write] --> GW
A[🛡 Admin<br/>admin] --> GW
GW --> Agent[🤖 Agent]
end

classDef viewer fill:#189AB4,stroke:#7C90A0,color:#fff
classDef ops fill:#10B981,stroke:#7C90A0,color:#fff
classDef admin fill:#8B0000,stroke:#7C90A0,color:#fff
classDef gateway fill:#F59E0B,stroke:#7C90A0,color:#fff
classDef agent fill:#6366F1,stroke:#7C90A0,color:#fff

class V viewer
class O ops
class A admin
class GW gateway
class Agent agent
```

## Quick Start

<Steps>
<Step title="Single-operator (no scopes — unchanged)">

Today's setup keeps working. Authenticated clients receive all scopes when no policy is configured.

```python
from praisonaiagents import Agent

agent = Agent(
name="assistant",
instructions="You are a helpful assistant.",
)

# $ praisonai gateway start --host 127.0.0.1
agent.start("hello")
```

</Step>

<Step title="Multi-operator (scoped tokens)">

Map each operator token to the scopes they need in `gateway.yaml`, then run your agent as usual.

```yaml
gateway:
host: "0.0.0.0"
port: 8765
auth:
tokens:
- token: "${VIEWER_TOKEN}"
scopes: [read]
- token: "${OPS_TOKEN}"
scopes: [read, write]
- token: "${ADMIN_TOKEN}"
scopes: [admin]

agents:
assistant:
instructions: "You are a helpful assistant."
model: gpt-4o-mini
```

```python
from praisonaiagents import Agent
from praisonaiagents.gateway import OperatorScope

agent = Agent(name="assistant", instructions="You are a helpful assistant.")
# OperatorScope.READ, .WRITE, .APPROVALS, .PAIRING, .ADMIN
print([s.value for s in OperatorScope.all()])
```

</Step>
</Steps>

<Note>
When **no** `auth_scopes` policy is configured, every successfully authenticated client is granted **all** scopes — identical to today's binary auth behaviour. Single-operator setups need no changes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Change "behaviour" to "behavior" to maintain US English spelling consistency with the rest of the documentation (e.g., "behavior" is used in gateway-bind-aware-auth.mdx).

</Note>

---

## How It Works

```mermaid
sequenceDiagram
participant Client
participant Gateway
participant Policy as resolve_scopes
participant Route as /api/approval/resolve
participant WS as WebSocket subscribers

Client->>Gateway: Connect with token
Gateway->>Policy: resolve_scopes(token)
Policy-->>Gateway: [read, approvals]
Client->>Route: POST resolve approval
alt has approvals scope
Route-->>Client: 200 OK
Gateway->>WS: approval event (approvals clients only)
else missing scope
Route-->>Client: 403 insufficient scope
end
```

1. Client connects with a bearer token.
2. Gateway resolves scopes via `GatewayConfig.resolve_scopes(token)`.
3. Each HTTP route and WebSocket action checks the required scope.
4. Outbound events are filtered — approval events only reach clients with the `approvals` scope.

---

## Scope Reference

| Scope | Value | Grants |
|---|---|---|
| Read | `read` | View dashboard, session transcripts, and status events |
| Write | `write` | Send messages as the agent (WebSocket `message`) |
| Approvals | `approvals` | Resolve tool-execution approvals and manage allowlist |
| Pairing | `pairing` | Approve or revoke device pairing |
| Admin | `admin` | Channel pause/resume/reconnect — implies all scopes |

```mermaid
graph TB
Admin[admin] --> Read[read]
Admin --> Write[write]
Admin --> Approvals[approvals]
Admin --> Pairing[pairing]

classDef admin fill:#8B0000,stroke:#7C90A0,color:#fff
classDef scope fill:#189AB4,stroke:#7C90A0,color:#fff

class Admin admin
class Read,Write,Approvals,Pairing scope
```

### Which scope should this operator have?

```mermaid
graph TD
Start{What do they need?}
Start -->|View only| R[read]
Start -->|Send messages| RW[read + write]
Start -->|Resolve approvals| RA[read + approvals]
Start -->|Manage pairing| RP[read + pairing]
Start -->|Full control| AD[admin]

classDef decision fill:#F59E0B,stroke:#7C90A0,color:#fff
classDef scope fill:#10B981,stroke:#7C90A0,color:#fff

class Start decision
class R,RW,RA,RP,AD scope
```

| Role | Recommended scopes |
|---|---|
| Read-only stakeholder | `[read]` |
| Junior support (send, not approve) | `[read, write]` |
| On-call approver | `[read, approvals]` |
| SRE / platform admin | `[admin]` |

---

## Configuration

### YAML — structured (recommended)

```yaml
gateway:
auth:
tokens:
- token: "${VIEWER_TOKEN}"
scopes: [read]
- token: "${OPS_TOKEN}"
scopes: [read, write, approvals]
- token: "${ADMIN_TOKEN}"
scopes: [admin]
```

### YAML — flat mapping

```yaml
gateway:
auth_scopes:
"${VIEWER_TOKEN}": [read]
"${OPS_TOKEN}": [read, write, approvals]
"${ADMIN_TOKEN}": [admin]
```

### Python

```python
from praisonaiagents.gateway import GatewayConfig, OperatorScope

config = GatewayConfig(
host="0.0.0.0",
port=8765,
auth_token="${ADMIN_TOKEN}",
auth_scopes={
"${VIEWER_TOKEN}": [OperatorScope.READ.value],
"${OPS_TOKEN}": [OperatorScope.READ.value, OperatorScope.WRITE.value],
"${ADMIN_TOKEN}": [OperatorScope.ADMIN.value],
},
)

print(config.has_scope_policy) # True when auth_scopes is non-empty
print(config.resolve_scopes("${VIEWER_TOKEN}")) # ['read']
```

---

## Scope-Gated Routes

| Route | Method | Required scope |
|---|---|---|
| `/api/channels/{name}/pause` | POST | `admin` |
| `/api/channels/{name}/resume` | POST | `admin` |
| `/api/channels/{name}/reconnect` | POST | `admin` |
| `/api/approval/resolve` | POST | `approvals` |
| `/api/approval/allowlist` | GET | any authenticated |
| `/api/approval/allowlist` | POST/DELETE | `approvals` |
Comment on lines +230 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The actual endpoint implemented in the gateway server is /api/approval/allow-list (with a hyphen), as defined in praisonai/gateway/server.py (line 992). Using /api/approval/allowlist (without a hyphen) will result in a 404 error. Please update the route paths in this table to use /api/approval/allow-list.

| `/api/pairing/approve` | POST | `pairing` |
| `/api/pairing/revoke` | POST | `pairing` |
| WebSocket `message` | — | `write` |

---

## Common Patterns

**Read-only dashboard viewer** — `[read]` for status and transcripts without send or approve rights.

**Send but not approve** — `[read, write]` for operators who reply to users but cannot resolve tool approvals.

**Approvals-only on-call** — `[read, approvals]` for security-sensitive approval resolution without channel admin rights.

**Full admin** — `[admin]` for SREs who need pause/resume/reconnect plus all other capabilities.

---

## Error Handling

HTTP 403 when scope check fails:

```json
{ "error": "insufficient scope", "required_scope": "approvals" }
```

WebSocket `message` without `write` scope:

```json
{
"type": "error",
"code": "insufficient_scope",
"message": "insufficient scope",
"required_scope": "write"
}
```

---

<Warning>
Granting `approvals` is effectively remote command execution — never assign it casually. `ALLOW_LOOPBACK_BYPASS=true` grants all scopes on loopback; use for local development only, never in production.
</Warning>

---

## Best Practices

<AccordionGroup>
<Accordion title="Default to read and add scopes as needed">
Start every operator with `[read]` and expand only when their role requires it.
</Accordion>

<Accordion title="Rotate per-token secrets independently">
Issue separate tokens per operator so you can revoke one role without rotating everyone.
</Accordion>

<Accordion title="Pair approvals with the allowlist">
Combine `approvals` scope with `/api/approval/allowlist` for defence-in-depth on tool execution.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update /api/approval/allowlist to /api/approval/allow-list to match the actual server endpoint. Additionally, consider changing "defence-in-depth" to "defense-in-depth" to maintain US English spelling consistency with the rest of the documentation.

</Accordion>

<Accordion title="Use admin sparingly">
Prefer explicit scope lists over `[admin]` unless the operator truly needs channel control.
</Accordion>
</AccordionGroup>

---

## Related

<CardGroup cols={2}>
<Card title="Bind-Aware Auth" icon="shield" href="/docs/features/gateway-bind-aware-auth">
Token requirements when binding to external interfaces
</Card>
<Card title="Gateway Overview" icon="broadcast-tower" href="/docs/features/gateway-overview">
Multi-channel gateway architecture and setup
</Card>
</CardGroup>