okta-agent 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- okta_agent-0.1.0/LICENSE +21 -0
- okta_agent-0.1.0/MANIFEST.in +4 -0
- okta_agent-0.1.0/PKG-INFO +243 -0
- okta_agent-0.1.0/README.md +213 -0
- okta_agent-0.1.0/okta_agent/__init__.py +68 -0
- okta_agent-0.1.0/okta_agent/__main__.py +4 -0
- okta_agent-0.1.0/okta_agent/agent_server.py +71 -0
- okta_agent-0.1.0/okta_agent/api/__init__.py +1 -0
- okta_agent-0.1.0/okta_agent/api/api_client_apps.py +250 -0
- okta_agent-0.1.0/okta_agent/api/api_client_base.py +301 -0
- okta_agent-0.1.0/okta_agent/api/api_client_groups.py +184 -0
- okta_agent-0.1.0/okta_agent/api/api_client_policies.py +112 -0
- okta_agent-0.1.0/okta_agent/api/api_client_system.py +95 -0
- okta_agent-0.1.0/okta_agent/api/api_client_users.py +250 -0
- okta_agent-0.1.0/okta_agent/api/credentials.py +161 -0
- okta_agent-0.1.0/okta_agent/api/filters.py +71 -0
- okta_agent-0.1.0/okta_agent/api_client.py +17 -0
- okta_agent-0.1.0/okta_agent/auth.py +94 -0
- okta_agent-0.1.0/okta_agent/main_agent.json +17 -0
- okta_agent-0.1.0/okta_agent/mcp/__init__.py +19 -0
- okta_agent-0.1.0/okta_agent/mcp/common.py +67 -0
- okta_agent-0.1.0/okta_agent/mcp/mcp_apps.py +127 -0
- okta_agent-0.1.0/okta_agent/mcp/mcp_groups.py +128 -0
- okta_agent-0.1.0/okta_agent/mcp/mcp_policies.py +124 -0
- okta_agent-0.1.0/okta_agent/mcp/mcp_system.py +81 -0
- okta_agent-0.1.0/okta_agent/mcp/mcp_users.py +162 -0
- okta_agent-0.1.0/okta_agent/mcp_config.json +3 -0
- okta_agent-0.1.0/okta_agent/mcp_server.py +70 -0
- okta_agent-0.1.0/okta_agent/okta_input_models.py +145 -0
- okta_agent-0.1.0/okta_agent/okta_response_models.py +72 -0
- okta_agent-0.1.0/okta_agent.egg-info/PKG-INFO +243 -0
- okta_agent-0.1.0/okta_agent.egg-info/SOURCES.txt +58 -0
- okta_agent-0.1.0/okta_agent.egg-info/dependency_links.txt +1 -0
- okta_agent-0.1.0/okta_agent.egg-info/entry_points.txt +3 -0
- okta_agent-0.1.0/okta_agent.egg-info/requires.txt +18 -0
- okta_agent-0.1.0/okta_agent.egg-info/top_level.txt +6 -0
- okta_agent-0.1.0/pyproject.toml +56 -0
- okta_agent-0.1.0/requirements.txt +3 -0
- okta_agent-0.1.0/scripts/security_sanitizer.py +160 -0
- okta_agent-0.1.0/scripts/validate_a2a_agent.py +139 -0
- okta_agent-0.1.0/scripts/validate_agent.py +31 -0
- okta_agent-0.1.0/scripts/verify_api_integration.py +281 -0
- okta_agent-0.1.0/setup.cfg +4 -0
- okta_agent-0.1.0/tests/__init__.py +0 -0
- okta_agent-0.1.0/tests/conftest.py +93 -0
- okta_agent-0.1.0/tests/test_api_wrapper.py +203 -0
- okta_agent-0.1.0/tests/test_apps_client.py +109 -0
- okta_agent-0.1.0/tests/test_auth.py +71 -0
- okta_agent-0.1.0/tests/test_concept_parity.py +33 -0
- okta_agent-0.1.0/tests/test_credentials.py +176 -0
- okta_agent-0.1.0/tests/test_filters.py +84 -0
- okta_agent-0.1.0/tests/test_groups_client.py +86 -0
- okta_agent-0.1.0/tests/test_init_dynamics.py +19 -0
- okta_agent-0.1.0/tests/test_okta_mcp_validation.py +201 -0
- okta_agent-0.1.0/tests/test_okta_models.py +94 -0
- okta_agent-0.1.0/tests/test_pagination.py +67 -0
- okta_agent-0.1.0/tests/test_policies_client.py +67 -0
- okta_agent-0.1.0/tests/test_startup.py +25 -0
- okta_agent-0.1.0/tests/test_system_client.py +66 -0
- okta_agent-0.1.0/tests/test_users_client.py +126 -0
okta_agent-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Advanced Agentic Coding
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: okta-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Okta CIAM/SSO MCP Server and Agent for Agentic AI!
|
|
5
|
+
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: <3.15,>=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: agent-utilities>=0.47.0
|
|
16
|
+
Requires-Dist: httpx>=0.27.0
|
|
17
|
+
Requires-Dist: PyJWT[crypto]>=2.8.0
|
|
18
|
+
Provides-Extra: mcp
|
|
19
|
+
Requires-Dist: agent-utilities[mcp]>=0.47.0; extra == "mcp"
|
|
20
|
+
Provides-Extra: agent
|
|
21
|
+
Requires-Dist: agent-utilities[agent,logfire]>=0.47.0; extra == "agent"
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Requires-Dist: okta-agent[agent,logfire,mcp]>=0.1.0; extra == "all"
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest-xdist>=3.6.0; extra == "test"
|
|
26
|
+
Requires-Dist: pytest; extra == "test"
|
|
27
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
28
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# Okta Agent
|
|
32
|
+
## CLI or API | MCP | Agent
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+

|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
*Version: 0.1.0*
|
|
43
|
+
|
|
44
|
+
> **Documentation** — Installation, deployment, usage across the API, CLI, and MCP
|
|
45
|
+
> server live on the docs site:
|
|
46
|
+
> <https://knuckles-team.github.io/okta-agent/>
|
|
47
|
+
|
|
48
|
+
## Table of Contents
|
|
49
|
+
|
|
50
|
+
- [Overview](#overview)
|
|
51
|
+
- [Architecture](#architecture)
|
|
52
|
+
- [Installation](#installation)
|
|
53
|
+
- [Configure](#configure)
|
|
54
|
+
- [Environment Variables](#environment-variables)
|
|
55
|
+
- [Quick Start](#quick-start)
|
|
56
|
+
- [Run](#run)
|
|
57
|
+
- [MCP Tools](#mcp-tools)
|
|
58
|
+
- [Deployment](#deployment)
|
|
59
|
+
- [Keycloak Parity](#keycloak-parity)
|
|
60
|
+
- [Development](#development)
|
|
61
|
+
|
|
62
|
+
## Overview
|
|
63
|
+
|
|
64
|
+
Enterprise CIAM/SSO connector for the agent fleet: a production-grade MCP
|
|
65
|
+
server and Pydantic AI agent over the **Okta Management API** — users, groups,
|
|
66
|
+
applications, policies, and the system log. Complements `keycloak-agent`
|
|
67
|
+
(open-source IdP) with the commercial-IdP side of the house, mirroring its
|
|
68
|
+
verb taxonomy so agents can switch IdPs with familiar verbs.
|
|
69
|
+
|
|
70
|
+
- Raw `httpx` client — no Okta SDK; every method documents the endpoint it calls.
|
|
71
|
+
- Two auth modes: **SSWS API token** and **OAuth2 private-key-JWT** (org
|
|
72
|
+
authorization server, Okta API scopes).
|
|
73
|
+
- Rate-limit aware: every response envelope carries the latest
|
|
74
|
+
`X-Rate-Limit-*` snapshot; HTTP 429 triggers capped automatic backoff.
|
|
75
|
+
- Cursor pagination via `Link: rel="next"` headers (Okta's `after` cursor),
|
|
76
|
+
with hard item caps and resumable `next_cursor`s.
|
|
77
|
+
- Safety: destructive operations (deactivate / delete / clear sessions /
|
|
78
|
+
password ops) are blocked unless explicitly allowed; credential material is
|
|
79
|
+
redacted from logs and error envelopes.
|
|
80
|
+
|
|
81
|
+
## Architecture
|
|
82
|
+
|
|
83
|
+
```mermaid
|
|
84
|
+
graph TD
|
|
85
|
+
User([User/A2A]) --> Server[A2A Server / okta-agent]
|
|
86
|
+
Server --> Agent[Pydantic AI Agent]
|
|
87
|
+
Agent --> MCP[MCP Server / okta-mcp]
|
|
88
|
+
MCP --> Client[Api facade / httpx]
|
|
89
|
+
Client --> ExternalAPI([Okta Management API])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install okta-agent # core API client
|
|
96
|
+
pip install okta-agent[mcp] # + MCP server
|
|
97
|
+
pip install okta-agent[agent] # + Pydantic AI agent server
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Configure
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
export OKTA_ORG_URL="https://acme.okta.com"
|
|
104
|
+
|
|
105
|
+
# Mode 1 — SSWS API token (takes precedence)
|
|
106
|
+
export OKTA_API_TOKEN="<api token>"
|
|
107
|
+
|
|
108
|
+
# Mode 2 — OAuth2 private-key-JWT (service app, Okta API scopes)
|
|
109
|
+
export OKTA_CLIENT_ID="0oa..."
|
|
110
|
+
export OKTA_PRIVATE_KEY_FILE="/path/to/key.pem" # or OKTA_PRIVATE_KEY inline
|
|
111
|
+
export OKTA_SCOPES="okta.users.read okta.groups.manage"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
See `.env.example` for every knob (`OKTA_SSL_VERIFY`, `OKTA_MAX_RETRIES`,
|
|
115
|
+
`OKTA_BACKOFF_CAP_SECONDS`, `OKTA_ALLOW_DESTRUCTIVE`, per-tool switches).
|
|
116
|
+
|
|
117
|
+
## Environment Variables
|
|
118
|
+
|
|
119
|
+
| Variable | Default | Purpose |
|
|
120
|
+
|----------|---------|---------|
|
|
121
|
+
| `OKTA_ORG_URL` | — | Okta org URL, no trailing slash (`OKTA_AGENT_BASE_URL` accepted as fallback) |
|
|
122
|
+
| `OKTA_API_TOKEN` | — | SSWS API token (auth mode 1, takes precedence) |
|
|
123
|
+
| `OKTA_CLIENT_ID` | — | Service-app client id (private-key-JWT) |
|
|
124
|
+
| `OKTA_PRIVATE_KEY` / `OKTA_PRIVATE_KEY_FILE` | — | PEM private key inline or path |
|
|
125
|
+
| `OKTA_KEY_ID` | — | Optional JWKS key id for the client assertion |
|
|
126
|
+
| `OKTA_SCOPES` | read scopes | Space-separated Okta API scopes |
|
|
127
|
+
| `OKTA_SSL_VERIFY` | `True` | TLS verification toward the org |
|
|
128
|
+
| `OKTA_MAX_RETRIES` | `2` | 429 retry attempts |
|
|
129
|
+
| `OKTA_BACKOFF_CAP_SECONDS` | `60` | 429 backoff cap |
|
|
130
|
+
| `OKTA_ALLOW_DESTRUCTIVE` | `False` | Org-wide default for destructive tool actions |
|
|
131
|
+
| `HOST` / `PORT` / `TRANSPORT` | `0.0.0.0` / `8000` / `stdio` | MCP server bind + transport |
|
|
132
|
+
| `USERSTOOL` / `GROUPSTOOL` / `APPSTOOL` / `POLICIESTOOL` / `SYSTEMTOOL` | `True` | Per-domain tool toggles |
|
|
133
|
+
| `ENABLE_OTEL` / `OTEL_EXPORTER_OTLP_*` | — | Telemetry (OTEL / Langfuse) |
|
|
134
|
+
| `EUNOMIA_TYPE` / `EUNOMIA_POLICY_FILE` / `EUNOMIA_REMOTE_URL` | `none` | MCP authorization middleware |
|
|
135
|
+
| `AUTH_TYPE` | `none` | MCP server auth mode (Docker) |
|
|
136
|
+
| `DEFAULT_AGENT_NAME` / `AGENT_DESCRIPTION` / `AGENT_SYSTEM_PROMPT` | identity files | A2A agent identity overrides |
|
|
137
|
+
|
|
138
|
+
## Quick Start
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from okta_agent import Api
|
|
142
|
+
from okta_agent.api.credentials import SswsToken
|
|
143
|
+
from okta_agent.okta_input_models import SearchInput, FilterCondition
|
|
144
|
+
|
|
145
|
+
api = Api(org_url="https://acme.okta.com", credential=SswsToken("<token>"))
|
|
146
|
+
|
|
147
|
+
active = api.search_users(
|
|
148
|
+
conditions=[{"field": "status", "op": "eq", "value": "ACTIVE"}],
|
|
149
|
+
)
|
|
150
|
+
print(active["data"], active["rate_limit"])
|
|
151
|
+
|
|
152
|
+
# Or build typed tool params for the MCP surface:
|
|
153
|
+
params = SearchInput(
|
|
154
|
+
conditions=[FilterCondition(field="status", op="eq", value="ACTIVE")]
|
|
155
|
+
).model_dump_json(exclude_none=True)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Run
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
okta-mcp # stdio MCP server
|
|
162
|
+
okta-mcp --transport streamable-http --host 0.0.0.0 --port 8000
|
|
163
|
+
okta-agent # A2A agent server
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## MCP Tools
|
|
167
|
+
|
|
168
|
+
Five consolidated, action-routed tools. Each takes `action`, `params_json`,
|
|
169
|
+
and (where applicable) `allow_destructive`.
|
|
170
|
+
|
|
171
|
+
| Tool | Actions | Destructive (gated) |
|
|
172
|
+
|------|---------|---------------------|
|
|
173
|
+
| `okta_users` | list, search, get, create, update, activate, deactivate, suspend, unsuspend, unlock, expire_password, reset_password, list_groups, list_apps, list_factors, clear_sessions | deactivate, suspend, expire_password, reset_password, clear_sessions |
|
|
174
|
+
| `okta_groups` | list, search, get, create, update, delete, list_members, add_member, remove_member, list_rules, create_rule, activate_rule, deactivate_rule | delete, remove_member, deactivate_rule |
|
|
175
|
+
| `okta_apps` | list, get, create (oidc/saml/bookmark templates), update, activate, deactivate, list_users, assign_user, unassign_user, list_groups, assign_group, unassign_group | deactivate, unassign_user, unassign_group |
|
|
176
|
+
| `okta_policies` | list (okta_sign_on/password/mfa_enroll/access_policy), get, list_rules, activate, deactivate, activate_rule, deactivate_rule — *policy/rule create+update intentionally out of scope in this release* | deactivate, deactivate_rule |
|
|
177
|
+
| `okta_system` | org, list_authorization_servers, logs (since/until/filter/q, capped at 1000 events, resumable cursor), list_authenticators, list_factors, list_zones | — |
|
|
178
|
+
|
|
179
|
+
### Examples
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{"action": "search", "params_json": "{\"conditions\": [{\"field\": \"status\", \"op\": \"eq\", \"value\": \"ACTIVE\"}, {\"field\": \"profile.department\", \"op\": \"eq\", \"value\": \"Engineering\"}]}"}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{"action": "create", "params_json": "{\"template\": \"oidc\", \"label\": \"My App\", \"settings\": {\"redirect_uris\": [\"https://app.example.com/cb\"]}}"}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{"action": "deactivate", "params_json": "{\"user_id\": \"00u1\"}", "allow_destructive": true}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Every successful response is an envelope:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{"data": [...], "rate_limit": {"limit": 600, "remaining": 599, "reset": 1700000000}, "count": 5, "truncated": false, "next_cursor": null}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Errors map Okta's envelope: `{"error": {"status", "error_code", "error_summary", "error_id", "error_causes", "rate_limit"}}`.
|
|
200
|
+
|
|
201
|
+
## Deployment
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# MCP server only (port 8000, streamable-http, /health)
|
|
205
|
+
docker compose -f docker/mcp.compose.yml up -d
|
|
206
|
+
|
|
207
|
+
# MCP server + A2A agent server (agent on port 9021, AG-UI web interface)
|
|
208
|
+
docker compose -f docker/agent.compose.yml up -d
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The A2A agent server (`okta-agent` console script, `agent_server.py`) reads
|
|
212
|
+
`MCP_URL`, `PROVIDER`, and `MODEL_ID` from the environment. Prebuilt image:
|
|
213
|
+
`knucklessg1/okta-agent:latest`. See [docs/deployment.md](docs/deployment.md)
|
|
214
|
+
for transports, reverse proxy, and DNS guidance.
|
|
215
|
+
|
|
216
|
+
## Keycloak parity
|
|
217
|
+
|
|
218
|
+
Verbs intentionally mirror `keycloak-agent` where the concepts overlap, so an
|
|
219
|
+
agent fluent in one IdP connector can drive the other:
|
|
220
|
+
|
|
221
|
+
| Concept | keycloak-agent | okta-agent |
|
|
222
|
+
|---------|----------------|------------|
|
|
223
|
+
| Users CRUD | `keycloak_agent_users` list/get/create/update | `okta_users` list/get/create/update |
|
|
224
|
+
| Password reset | `reset_password` | `reset_password` (plus expire_password) |
|
|
225
|
+
| User's groups | `list_users_by_user_id_groups` | `list_groups` |
|
|
226
|
+
| Groups CRUD + members | `keycloak_agent_groups` | `okta_groups` (+ Okta group rules) |
|
|
227
|
+
| OAuth/SAML clients | `keycloak_agent_clients` | `okta_apps` (Keycloak "clients" = Okta "apps") |
|
|
228
|
+
| Auth policies/flows | `keycloak_agent_authentication` | `okta_policies` (read + lifecycle) |
|
|
229
|
+
| Server/org info & events | `keycloak_agent_info` / attack detection | `okta_system` (org + system log) |
|
|
230
|
+
|
|
231
|
+
Okta-only surface: lifecycle states (suspend/unlock), group rules, app
|
|
232
|
+
assignment profiles, network zones, Okta API scopes via private-key-JWT.
|
|
233
|
+
|
|
234
|
+
## Development
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
pip install -e .[mcp,test]
|
|
238
|
+
pytest -v
|
|
239
|
+
pre-commit run --all-files
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
API references are cited in every client docstring
|
|
243
|
+
(https://developer.okta.com/docs/api/).
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Okta Agent
|
|
2
|
+
## CLI or API | MCP | Agent
|
|
3
|
+
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
*Version: 0.1.0*
|
|
13
|
+
|
|
14
|
+
> **Documentation** — Installation, deployment, usage across the API, CLI, and MCP
|
|
15
|
+
> server live on the docs site:
|
|
16
|
+
> <https://knuckles-team.github.io/okta-agent/>
|
|
17
|
+
|
|
18
|
+
## Table of Contents
|
|
19
|
+
|
|
20
|
+
- [Overview](#overview)
|
|
21
|
+
- [Architecture](#architecture)
|
|
22
|
+
- [Installation](#installation)
|
|
23
|
+
- [Configure](#configure)
|
|
24
|
+
- [Environment Variables](#environment-variables)
|
|
25
|
+
- [Quick Start](#quick-start)
|
|
26
|
+
- [Run](#run)
|
|
27
|
+
- [MCP Tools](#mcp-tools)
|
|
28
|
+
- [Deployment](#deployment)
|
|
29
|
+
- [Keycloak Parity](#keycloak-parity)
|
|
30
|
+
- [Development](#development)
|
|
31
|
+
|
|
32
|
+
## Overview
|
|
33
|
+
|
|
34
|
+
Enterprise CIAM/SSO connector for the agent fleet: a production-grade MCP
|
|
35
|
+
server and Pydantic AI agent over the **Okta Management API** — users, groups,
|
|
36
|
+
applications, policies, and the system log. Complements `keycloak-agent`
|
|
37
|
+
(open-source IdP) with the commercial-IdP side of the house, mirroring its
|
|
38
|
+
verb taxonomy so agents can switch IdPs with familiar verbs.
|
|
39
|
+
|
|
40
|
+
- Raw `httpx` client — no Okta SDK; every method documents the endpoint it calls.
|
|
41
|
+
- Two auth modes: **SSWS API token** and **OAuth2 private-key-JWT** (org
|
|
42
|
+
authorization server, Okta API scopes).
|
|
43
|
+
- Rate-limit aware: every response envelope carries the latest
|
|
44
|
+
`X-Rate-Limit-*` snapshot; HTTP 429 triggers capped automatic backoff.
|
|
45
|
+
- Cursor pagination via `Link: rel="next"` headers (Okta's `after` cursor),
|
|
46
|
+
with hard item caps and resumable `next_cursor`s.
|
|
47
|
+
- Safety: destructive operations (deactivate / delete / clear sessions /
|
|
48
|
+
password ops) are blocked unless explicitly allowed; credential material is
|
|
49
|
+
redacted from logs and error envelopes.
|
|
50
|
+
|
|
51
|
+
## Architecture
|
|
52
|
+
|
|
53
|
+
```mermaid
|
|
54
|
+
graph TD
|
|
55
|
+
User([User/A2A]) --> Server[A2A Server / okta-agent]
|
|
56
|
+
Server --> Agent[Pydantic AI Agent]
|
|
57
|
+
Agent --> MCP[MCP Server / okta-mcp]
|
|
58
|
+
MCP --> Client[Api facade / httpx]
|
|
59
|
+
Client --> ExternalAPI([Okta Management API])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install okta-agent # core API client
|
|
66
|
+
pip install okta-agent[mcp] # + MCP server
|
|
67
|
+
pip install okta-agent[agent] # + Pydantic AI agent server
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configure
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
export OKTA_ORG_URL="https://acme.okta.com"
|
|
74
|
+
|
|
75
|
+
# Mode 1 — SSWS API token (takes precedence)
|
|
76
|
+
export OKTA_API_TOKEN="<api token>"
|
|
77
|
+
|
|
78
|
+
# Mode 2 — OAuth2 private-key-JWT (service app, Okta API scopes)
|
|
79
|
+
export OKTA_CLIENT_ID="0oa..."
|
|
80
|
+
export OKTA_PRIVATE_KEY_FILE="/path/to/key.pem" # or OKTA_PRIVATE_KEY inline
|
|
81
|
+
export OKTA_SCOPES="okta.users.read okta.groups.manage"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
See `.env.example` for every knob (`OKTA_SSL_VERIFY`, `OKTA_MAX_RETRIES`,
|
|
85
|
+
`OKTA_BACKOFF_CAP_SECONDS`, `OKTA_ALLOW_DESTRUCTIVE`, per-tool switches).
|
|
86
|
+
|
|
87
|
+
## Environment Variables
|
|
88
|
+
|
|
89
|
+
| Variable | Default | Purpose |
|
|
90
|
+
|----------|---------|---------|
|
|
91
|
+
| `OKTA_ORG_URL` | — | Okta org URL, no trailing slash (`OKTA_AGENT_BASE_URL` accepted as fallback) |
|
|
92
|
+
| `OKTA_API_TOKEN` | — | SSWS API token (auth mode 1, takes precedence) |
|
|
93
|
+
| `OKTA_CLIENT_ID` | — | Service-app client id (private-key-JWT) |
|
|
94
|
+
| `OKTA_PRIVATE_KEY` / `OKTA_PRIVATE_KEY_FILE` | — | PEM private key inline or path |
|
|
95
|
+
| `OKTA_KEY_ID` | — | Optional JWKS key id for the client assertion |
|
|
96
|
+
| `OKTA_SCOPES` | read scopes | Space-separated Okta API scopes |
|
|
97
|
+
| `OKTA_SSL_VERIFY` | `True` | TLS verification toward the org |
|
|
98
|
+
| `OKTA_MAX_RETRIES` | `2` | 429 retry attempts |
|
|
99
|
+
| `OKTA_BACKOFF_CAP_SECONDS` | `60` | 429 backoff cap |
|
|
100
|
+
| `OKTA_ALLOW_DESTRUCTIVE` | `False` | Org-wide default for destructive tool actions |
|
|
101
|
+
| `HOST` / `PORT` / `TRANSPORT` | `0.0.0.0` / `8000` / `stdio` | MCP server bind + transport |
|
|
102
|
+
| `USERSTOOL` / `GROUPSTOOL` / `APPSTOOL` / `POLICIESTOOL` / `SYSTEMTOOL` | `True` | Per-domain tool toggles |
|
|
103
|
+
| `ENABLE_OTEL` / `OTEL_EXPORTER_OTLP_*` | — | Telemetry (OTEL / Langfuse) |
|
|
104
|
+
| `EUNOMIA_TYPE` / `EUNOMIA_POLICY_FILE` / `EUNOMIA_REMOTE_URL` | `none` | MCP authorization middleware |
|
|
105
|
+
| `AUTH_TYPE` | `none` | MCP server auth mode (Docker) |
|
|
106
|
+
| `DEFAULT_AGENT_NAME` / `AGENT_DESCRIPTION` / `AGENT_SYSTEM_PROMPT` | identity files | A2A agent identity overrides |
|
|
107
|
+
|
|
108
|
+
## Quick Start
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from okta_agent import Api
|
|
112
|
+
from okta_agent.api.credentials import SswsToken
|
|
113
|
+
from okta_agent.okta_input_models import SearchInput, FilterCondition
|
|
114
|
+
|
|
115
|
+
api = Api(org_url="https://acme.okta.com", credential=SswsToken("<token>"))
|
|
116
|
+
|
|
117
|
+
active = api.search_users(
|
|
118
|
+
conditions=[{"field": "status", "op": "eq", "value": "ACTIVE"}],
|
|
119
|
+
)
|
|
120
|
+
print(active["data"], active["rate_limit"])
|
|
121
|
+
|
|
122
|
+
# Or build typed tool params for the MCP surface:
|
|
123
|
+
params = SearchInput(
|
|
124
|
+
conditions=[FilterCondition(field="status", op="eq", value="ACTIVE")]
|
|
125
|
+
).model_dump_json(exclude_none=True)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Run
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
okta-mcp # stdio MCP server
|
|
132
|
+
okta-mcp --transport streamable-http --host 0.0.0.0 --port 8000
|
|
133
|
+
okta-agent # A2A agent server
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## MCP Tools
|
|
137
|
+
|
|
138
|
+
Five consolidated, action-routed tools. Each takes `action`, `params_json`,
|
|
139
|
+
and (where applicable) `allow_destructive`.
|
|
140
|
+
|
|
141
|
+
| Tool | Actions | Destructive (gated) |
|
|
142
|
+
|------|---------|---------------------|
|
|
143
|
+
| `okta_users` | list, search, get, create, update, activate, deactivate, suspend, unsuspend, unlock, expire_password, reset_password, list_groups, list_apps, list_factors, clear_sessions | deactivate, suspend, expire_password, reset_password, clear_sessions |
|
|
144
|
+
| `okta_groups` | list, search, get, create, update, delete, list_members, add_member, remove_member, list_rules, create_rule, activate_rule, deactivate_rule | delete, remove_member, deactivate_rule |
|
|
145
|
+
| `okta_apps` | list, get, create (oidc/saml/bookmark templates), update, activate, deactivate, list_users, assign_user, unassign_user, list_groups, assign_group, unassign_group | deactivate, unassign_user, unassign_group |
|
|
146
|
+
| `okta_policies` | list (okta_sign_on/password/mfa_enroll/access_policy), get, list_rules, activate, deactivate, activate_rule, deactivate_rule — *policy/rule create+update intentionally out of scope in this release* | deactivate, deactivate_rule |
|
|
147
|
+
| `okta_system` | org, list_authorization_servers, logs (since/until/filter/q, capped at 1000 events, resumable cursor), list_authenticators, list_factors, list_zones | — |
|
|
148
|
+
|
|
149
|
+
### Examples
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{"action": "search", "params_json": "{\"conditions\": [{\"field\": \"status\", \"op\": \"eq\", \"value\": \"ACTIVE\"}, {\"field\": \"profile.department\", \"op\": \"eq\", \"value\": \"Engineering\"}]}"}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{"action": "create", "params_json": "{\"template\": \"oidc\", \"label\": \"My App\", \"settings\": {\"redirect_uris\": [\"https://app.example.com/cb\"]}}"}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{"action": "deactivate", "params_json": "{\"user_id\": \"00u1\"}", "allow_destructive": true}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Every successful response is an envelope:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{"data": [...], "rate_limit": {"limit": 600, "remaining": 599, "reset": 1700000000}, "count": 5, "truncated": false, "next_cursor": null}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Errors map Okta's envelope: `{"error": {"status", "error_code", "error_summary", "error_id", "error_causes", "rate_limit"}}`.
|
|
170
|
+
|
|
171
|
+
## Deployment
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# MCP server only (port 8000, streamable-http, /health)
|
|
175
|
+
docker compose -f docker/mcp.compose.yml up -d
|
|
176
|
+
|
|
177
|
+
# MCP server + A2A agent server (agent on port 9021, AG-UI web interface)
|
|
178
|
+
docker compose -f docker/agent.compose.yml up -d
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The A2A agent server (`okta-agent` console script, `agent_server.py`) reads
|
|
182
|
+
`MCP_URL`, `PROVIDER`, and `MODEL_ID` from the environment. Prebuilt image:
|
|
183
|
+
`knucklessg1/okta-agent:latest`. See [docs/deployment.md](docs/deployment.md)
|
|
184
|
+
for transports, reverse proxy, and DNS guidance.
|
|
185
|
+
|
|
186
|
+
## Keycloak parity
|
|
187
|
+
|
|
188
|
+
Verbs intentionally mirror `keycloak-agent` where the concepts overlap, so an
|
|
189
|
+
agent fluent in one IdP connector can drive the other:
|
|
190
|
+
|
|
191
|
+
| Concept | keycloak-agent | okta-agent |
|
|
192
|
+
|---------|----------------|------------|
|
|
193
|
+
| Users CRUD | `keycloak_agent_users` list/get/create/update | `okta_users` list/get/create/update |
|
|
194
|
+
| Password reset | `reset_password` | `reset_password` (plus expire_password) |
|
|
195
|
+
| User's groups | `list_users_by_user_id_groups` | `list_groups` |
|
|
196
|
+
| Groups CRUD + members | `keycloak_agent_groups` | `okta_groups` (+ Okta group rules) |
|
|
197
|
+
| OAuth/SAML clients | `keycloak_agent_clients` | `okta_apps` (Keycloak "clients" = Okta "apps") |
|
|
198
|
+
| Auth policies/flows | `keycloak_agent_authentication` | `okta_policies` (read + lifecycle) |
|
|
199
|
+
| Server/org info & events | `keycloak_agent_info` / attack detection | `okta_system` (org + system log) |
|
|
200
|
+
|
|
201
|
+
Okta-only surface: lifecycle states (suspend/unlock), group rules, app
|
|
202
|
+
assignment profiles, network zones, Okta API scopes via private-key-JWT.
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
pip install -e .[mcp,test]
|
|
208
|
+
pytest -v
|
|
209
|
+
pre-commit run --all-files
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
API references are cited in every client docstring
|
|
213
|
+
(https://developer.okta.com/docs/api/).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""CONCEPT:ECO-4.0 okta-agent: Okta Management API + MCP Server + A2A Server."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__all__: list[str] = []
|
|
9
|
+
|
|
10
|
+
CORE_MODULES = [
|
|
11
|
+
"okta_agent.okta_input_models",
|
|
12
|
+
"okta_agent.okta_response_models",
|
|
13
|
+
"okta_agent.api_client",
|
|
14
|
+
]
|
|
15
|
+
OPTIONAL_MODULES = {
|
|
16
|
+
"okta_agent.agent_server": "agent",
|
|
17
|
+
"okta_agent.mcp_server": "mcp",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _expose_members(module):
|
|
22
|
+
for name, obj in inspect.getmembers(module):
|
|
23
|
+
if (inspect.isclass(obj) or inspect.isfunction(obj)) and not name.startswith(
|
|
24
|
+
"_"
|
|
25
|
+
):
|
|
26
|
+
globals()[name] = obj
|
|
27
|
+
if name not in __all__:
|
|
28
|
+
__all__.append(name)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
for module_name in CORE_MODULES:
|
|
32
|
+
module = importlib.import_module(module_name)
|
|
33
|
+
_expose_members(module)
|
|
34
|
+
|
|
35
|
+
_loaded_optional_modules: dict[str, Any] = {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _import_module_safely(module_name: str):
|
|
39
|
+
try:
|
|
40
|
+
return importlib.import_module(module_name)
|
|
41
|
+
except ImportError:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __getattr__(name: str) -> Any:
|
|
46
|
+
if name == "_MCP_AVAILABLE":
|
|
47
|
+
mcp_key = next((k for k in OPTIONAL_MODULES if "mcp_server" in k), None)
|
|
48
|
+
return _import_module_safely(mcp_key) is not None if mcp_key else False
|
|
49
|
+
if name == "_AGENT_AVAILABLE":
|
|
50
|
+
agent_key = next((k for k in OPTIONAL_MODULES if "agent_server" in k), None)
|
|
51
|
+
return _import_module_safely(agent_key) is not None if agent_key else False
|
|
52
|
+
|
|
53
|
+
for module_name in OPTIONAL_MODULES:
|
|
54
|
+
if module_name not in _loaded_optional_modules:
|
|
55
|
+
module = _import_module_safely(module_name)
|
|
56
|
+
if module is not None:
|
|
57
|
+
_loaded_optional_modules[module_name] = module
|
|
58
|
+
_expose_members(module)
|
|
59
|
+
|
|
60
|
+
module = _loaded_optional_modules.get(module_name)
|
|
61
|
+
if module is not None and hasattr(module, name):
|
|
62
|
+
return getattr(module, name)
|
|
63
|
+
|
|
64
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def __dir__() -> list[str]:
|
|
68
|
+
return sorted(list(globals().keys()) + __all__)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Pydantic AI agent server entry point for the Okta connector."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
logging.basicConfig(
|
|
11
|
+
level=logging.INFO,
|
|
12
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
13
|
+
)
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
DEFAULT_AGENT_NAME = None
|
|
17
|
+
DEFAULT_AGENT_DESCRIPTION = None
|
|
18
|
+
DEFAULT_AGENT_SYSTEM_PROMPT = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def agent_server():
|
|
22
|
+
"""Start the graph-based Pydantic AI agent server."""
|
|
23
|
+
from agent_utilities import (
|
|
24
|
+
build_system_prompt_from_workspace,
|
|
25
|
+
create_agent_parser,
|
|
26
|
+
create_agent_server,
|
|
27
|
+
initialize_workspace,
|
|
28
|
+
load_identity,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
global DEFAULT_AGENT_NAME, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_SYSTEM_PROMPT
|
|
32
|
+
initialize_workspace()
|
|
33
|
+
meta = load_identity()
|
|
34
|
+
DEFAULT_AGENT_NAME = os.getenv("DEFAULT_AGENT_NAME", meta.get("name", "Okta Agent"))
|
|
35
|
+
DEFAULT_AGENT_DESCRIPTION = os.getenv(
|
|
36
|
+
"AGENT_DESCRIPTION",
|
|
37
|
+
meta.get("description", "AI agent for Okta CIAM/SSO operations."),
|
|
38
|
+
)
|
|
39
|
+
DEFAULT_AGENT_SYSTEM_PROMPT = os.getenv(
|
|
40
|
+
"AGENT_SYSTEM_PROMPT",
|
|
41
|
+
meta.get("content") or build_system_prompt_from_workspace(),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
warnings.filterwarnings("ignore", message=".*urllib3.*")
|
|
45
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning, module="fastmcp")
|
|
46
|
+
|
|
47
|
+
print(f"{DEFAULT_AGENT_NAME} v{__version__}", file=sys.stderr)
|
|
48
|
+
parser = create_agent_parser()
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
51
|
+
create_agent_server(
|
|
52
|
+
mcp_url=args.mcp_url,
|
|
53
|
+
mcp_config=args.mcp_config or "mcp_config.json",
|
|
54
|
+
host=args.host,
|
|
55
|
+
port=args.port,
|
|
56
|
+
provider=args.provider,
|
|
57
|
+
model_id=args.model_id,
|
|
58
|
+
router_model=args.model_id,
|
|
59
|
+
agent_model=args.model_id,
|
|
60
|
+
base_url=args.base_url,
|
|
61
|
+
api_key=args.api_key,
|
|
62
|
+
custom_skills_directory=args.custom_skills_directory,
|
|
63
|
+
enable_web_ui=args.web,
|
|
64
|
+
enable_otel=args.otel,
|
|
65
|
+
otel_endpoint=args.otel_endpoint,
|
|
66
|
+
otel_headers=args.otel_headers,
|
|
67
|
+
otel_public_key=args.otel_public_key,
|
|
68
|
+
otel_secret_key=args.otel_secret_key,
|
|
69
|
+
otel_protocol=args.otel_protocol,
|
|
70
|
+
debug=args.debug,
|
|
71
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Okta Management API client modules (raw httpx, no Okta SDK)."""
|