identity-aiops 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.
- identity_aiops-0.1.0/.github/workflows/mcp-publish.yml +55 -0
- identity_aiops-0.1.0/.github/workflows/publish.yml +26 -0
- identity_aiops-0.1.0/.gitignore +8 -0
- identity_aiops-0.1.0/CHANGELOG.md +33 -0
- identity_aiops-0.1.0/LICENSE +21 -0
- identity_aiops-0.1.0/PKG-INFO +172 -0
- identity_aiops-0.1.0/README.md +156 -0
- identity_aiops-0.1.0/RELEASE_NOTES.md +68 -0
- identity_aiops-0.1.0/SECURITY.md +83 -0
- identity_aiops-0.1.0/identity_aiops/__init__.py +14 -0
- identity_aiops-0.1.0/identity_aiops/cli/__init__.py +9 -0
- identity_aiops-0.1.0/identity_aiops/cli/_common.py +78 -0
- identity_aiops-0.1.0/identity_aiops/cli/_root.py +62 -0
- identity_aiops-0.1.0/identity_aiops/cli/clients.py +99 -0
- identity_aiops-0.1.0/identity_aiops/cli/doctor.py +21 -0
- identity_aiops-0.1.0/identity_aiops/cli/events.py +30 -0
- identity_aiops-0.1.0/identity_aiops/cli/init.py +159 -0
- identity_aiops-0.1.0/identity_aiops/cli/overview.py +16 -0
- identity_aiops-0.1.0/identity_aiops/cli/secret.py +105 -0
- identity_aiops-0.1.0/identity_aiops/cli/undo.py +62 -0
- identity_aiops-0.1.0/identity_aiops/cli/users.py +163 -0
- identity_aiops-0.1.0/identity_aiops/config.py +164 -0
- identity_aiops-0.1.0/identity_aiops/connection.py +301 -0
- identity_aiops-0.1.0/identity_aiops/doctor.py +93 -0
- identity_aiops-0.1.0/identity_aiops/governance/__init__.py +40 -0
- identity_aiops-0.1.0/identity_aiops/governance/audit.py +377 -0
- identity_aiops-0.1.0/identity_aiops/governance/budget.py +225 -0
- identity_aiops-0.1.0/identity_aiops/governance/decorators.py +482 -0
- identity_aiops-0.1.0/identity_aiops/governance/paths.py +23 -0
- identity_aiops-0.1.0/identity_aiops/governance/patterns.py +378 -0
- identity_aiops-0.1.0/identity_aiops/governance/policy.py +430 -0
- identity_aiops-0.1.0/identity_aiops/governance/sanitize.py +45 -0
- identity_aiops-0.1.0/identity_aiops/governance/undo.py +218 -0
- identity_aiops-0.1.0/identity_aiops/ops/__init__.py +1 -0
- identity_aiops-0.1.0/identity_aiops/ops/_util.py +117 -0
- identity_aiops-0.1.0/identity_aiops/ops/analysis.py +573 -0
- identity_aiops-0.1.0/identity_aiops/ops/clients.py +142 -0
- identity_aiops-0.1.0/identity_aiops/ops/events.py +142 -0
- identity_aiops-0.1.0/identity_aiops/ops/overview.py +62 -0
- identity_aiops-0.1.0/identity_aiops/ops/realm.py +67 -0
- identity_aiops-0.1.0/identity_aiops/ops/users.py +202 -0
- identity_aiops-0.1.0/identity_aiops/ops/writes.py +228 -0
- identity_aiops-0.1.0/identity_aiops/platform.py +268 -0
- identity_aiops-0.1.0/identity_aiops/secretstore.py +304 -0
- identity_aiops-0.1.0/mcp_server/__init__.py +1 -0
- identity_aiops-0.1.0/mcp_server/_shared.py +107 -0
- identity_aiops-0.1.0/mcp_server/server.py +40 -0
- identity_aiops-0.1.0/mcp_server/tools/__init__.py +1 -0
- identity_aiops-0.1.0/mcp_server/tools/analysis.py +95 -0
- identity_aiops-0.1.0/mcp_server/tools/clients.py +64 -0
- identity_aiops-0.1.0/mcp_server/tools/events.py +41 -0
- identity_aiops-0.1.0/mcp_server/tools/system.py +46 -0
- identity_aiops-0.1.0/mcp_server/tools/undo.py +121 -0
- identity_aiops-0.1.0/mcp_server/tools/users.py +120 -0
- identity_aiops-0.1.0/mcp_server/tools/writes.py +250 -0
- identity_aiops-0.1.0/pyproject.toml +61 -0
- identity_aiops-0.1.0/server.json +21 -0
- identity_aiops-0.1.0/skills/identity-aiops/SKILL.md +156 -0
- identity_aiops-0.1.0/skills/identity-aiops/references/capabilities.md +74 -0
- identity_aiops-0.1.0/skills/identity-aiops/references/cli-reference.md +75 -0
- identity_aiops-0.1.0/skills/identity-aiops/references/setup-guide.md +108 -0
- identity_aiops-0.1.0/smithery.yaml +9 -0
- identity_aiops-0.1.0/tests/conftest.py +34 -0
- identity_aiops-0.1.0/tests/test_analysis.py +254 -0
- identity_aiops-0.1.0/tests/test_cli_writes.py +105 -0
- identity_aiops-0.1.0/tests/test_doctor.py +225 -0
- identity_aiops-0.1.0/tests/test_governance_persistence.py +179 -0
- identity_aiops-0.1.0/tests/test_init.py +155 -0
- identity_aiops-0.1.0/tests/test_platform.py +240 -0
- identity_aiops-0.1.0/tests/test_reads.py +320 -0
- identity_aiops-0.1.0/tests/test_secretstore.py +99 -0
- identity_aiops-0.1.0/tests/test_smoke.py +128 -0
- identity_aiops-0.1.0/tests/test_undo_executor.py +138 -0
- identity_aiops-0.1.0/tests/test_writes.py +388 -0
- identity_aiops-0.1.0/uv.lock +1121 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: mcp-publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
release:
|
|
6
|
+
types: [published]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish-mcp:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Wait for PyPI
|
|
18
|
+
# The release event also triggers the PyPI publish workflow; the MCP
|
|
19
|
+
# registry validates that the package version exists on PyPI, so poll
|
|
20
|
+
# until it has propagated (every 15s, up to 10 minutes).
|
|
21
|
+
run: |
|
|
22
|
+
python3 - <<'EOF'
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
import urllib.error
|
|
27
|
+
import urllib.request
|
|
28
|
+
|
|
29
|
+
with open("server.json", encoding="utf-8") as f:
|
|
30
|
+
package = json.load(f)["packages"][0]
|
|
31
|
+
name, version = package["identifier"], package["version"]
|
|
32
|
+
url = f"https://pypi.org/pypi/{name}/{version}/json"
|
|
33
|
+
deadline = time.monotonic() + 600
|
|
34
|
+
while True:
|
|
35
|
+
try:
|
|
36
|
+
with urllib.request.urlopen(url, timeout=10):
|
|
37
|
+
print(f"{name}=={version} is available on PyPI.")
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
40
|
+
print(f"{name}=={version} not on PyPI yet ({exc}); "
|
|
41
|
+
"retrying in 15s...")
|
|
42
|
+
if time.monotonic() >= deadline:
|
|
43
|
+
sys.exit(f"Timed out after 10 minutes waiting for "
|
|
44
|
+
f"{name}=={version} to appear on PyPI. "
|
|
45
|
+
"Check the PyPI publish workflow, then re-run "
|
|
46
|
+
"this workflow via workflow_dispatch.")
|
|
47
|
+
time.sleep(15)
|
|
48
|
+
EOF
|
|
49
|
+
- name: Install mcp-publisher
|
|
50
|
+
run: |
|
|
51
|
+
curl -sL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
52
|
+
- name: Login to MCP Registry (GitHub OIDC)
|
|
53
|
+
run: ./mcp-publisher login github-oidc
|
|
54
|
+
- name: Publish server.json
|
|
55
|
+
run: ./mcp-publisher publish
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Trusted Publishing (OIDC) — publishes from GitHub's runners with no API token,
|
|
4
|
+
# sidestepping the local-IP / account new-project rate limit. Configure a matching
|
|
5
|
+
# "trusted publisher" for this package on PyPI (see the repo release notes).
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [published]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
publish:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
id-token: write # required for PyPI Trusted Publishing (OIDC)
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Set up uv
|
|
22
|
+
uses: astral-sh/setup-uv@v5
|
|
23
|
+
- name: Build sdist + wheel
|
|
24
|
+
run: uv build
|
|
25
|
+
- name: Publish to PyPI (Trusted Publishing)
|
|
26
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v0.1.0 — 2026-07-17
|
|
4
|
+
|
|
5
|
+
Initial preview release.
|
|
6
|
+
|
|
7
|
+
- **27 MCP tools** (21 read incl. 4 flagship analyses, 6 governed writes) across
|
|
8
|
+
**Keycloak** (admin REST API, client-credentials grant with refresh-on-401)
|
|
9
|
+
and **authentik** (API v3, Bearer token), selected per target by a
|
|
10
|
+
name-keyed platform registry.
|
|
11
|
+
- **Flagship analyses**: `login_failure_rca` (password spray / targeted
|
|
12
|
+
brute-force / stale stored credential / misconfigured client /
|
|
13
|
+
expired-credential storm / lockout storm), `stale_access_audit` (idle users,
|
|
14
|
+
never-logged-in accounts, interactive service accounts, orphaned sessions),
|
|
15
|
+
`client_misconfig_audit` (wildcard/http redirect URIs, public clients with
|
|
16
|
+
secrets, implicit flow, missing PKCE, password grant — ranked riskScore),
|
|
17
|
+
`mfa_coverage_analysis` (overall + per-group coverage %, gap list).
|
|
18
|
+
- **Governed writes** with dry-run previews, fetched prior state, and undo where
|
|
19
|
+
reversible: `disable_user` / `enable_user` (undo pair),
|
|
20
|
+
`revoke_user_sessions` (priorState session count, no undo),
|
|
21
|
+
`require_password_reset` (undo clears the flag via `clear=True`),
|
|
22
|
+
`update_client_redirect_uris` (undo replays the prior list),
|
|
23
|
+
`rotate_client_secret` (masked priorState, no undo).
|
|
24
|
+
- **Secure by default**: with no `rules.yaml`, high-risk writes require a named
|
|
25
|
+
approver (`IDENTITY_AUDIT_APPROVED_BY`); `init` seeds a dual-control starter
|
|
26
|
+
rules file (never clobbers an operator-authored one).
|
|
27
|
+
- **Encrypted secret store** (`secrets.enc`, Fernet + scrypt); TLS verification
|
|
28
|
+
defaults ON; central percent-encoding of every URL path segment.
|
|
29
|
+
- Vendored governance harness (audit / budget / risk tiers / undo / sanitize) —
|
|
30
|
+
zero external skill-family dependencies.
|
|
31
|
+
- Preview / mock-only: modelled on the public Keycloak and authentik APIs,
|
|
32
|
+
validated against mocked responses; `identity-aiops doctor` (token
|
|
33
|
+
acquisition + user-count probe) is the fastest live check.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wei <zhouwei008@gmail.com>
|
|
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,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: identity-aiops
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Governed AI-ops for Keycloak + authentik identity providers: users, sessions, auth events, OAuth clients, flagship RCA analyses (login-failure, stale access, client misconfig, MFA coverage), and governed identity writes (disable/enable, session revoke, reset, redirect URIs, secret rotation) with a built-in governance harness (audit, budget, undo, risk tiers)
|
|
5
|
+
Author-email: wei <zhouwei008@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: cryptography>=42.0
|
|
10
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
11
|
+
Requires-Dist: mcp[cli]<2.0,>=1.10
|
|
12
|
+
Requires-Dist: pyyaml<7.0,>=6.0
|
|
13
|
+
Requires-Dist: rich<16.0,>=13.0
|
|
14
|
+
Requires-Dist: typer<1.0,>=0.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Identity AIops
|
|
18
|
+
|
|
19
|
+
**Governed AI-ops for self-hosted identity providers — Keycloak + authentik.**
|
|
20
|
+
|
|
21
|
+
`identity-aiops` gives AI agents (and the humans supervising them) a safe,
|
|
22
|
+
audited way to operate the identity plane of self-built and small/mid-size
|
|
23
|
+
infrastructure: who can sign in, what failed and why, which OAuth clients are
|
|
24
|
+
misconfigured, and who still has no second factor. It is built for teams
|
|
25
|
+
running their own Keycloak or authentik who want agent-driven identity
|
|
26
|
+
operations **with receipts** — every tool call is audited, writes carry risk
|
|
27
|
+
tiers with dry-run previews and undo tokens, and high-risk actions require a
|
|
28
|
+
named human approver.
|
|
29
|
+
|
|
30
|
+
> **Preview**: modelled on the public Keycloak admin REST API and authentik API
|
|
31
|
+
> v3 and exercised against mocked responses; not yet validated against live
|
|
32
|
+
> production instances. `identity-aiops doctor` is the fastest live check.
|
|
33
|
+
|
|
34
|
+
## What it does
|
|
35
|
+
|
|
36
|
+
| Area | Tools |
|
|
37
|
+
|------|-------|
|
|
38
|
+
| Realm / system | overview, realm settings (brute-force protection, password/OTP policy), identity providers |
|
|
39
|
+
| Users | list/search, detail, count, sessions, credentials (MFA surface), groups, members, lockout status |
|
|
40
|
+
| Events | authentication events, admin/config-change events |
|
|
41
|
+
| Clients | list, detail, per-client sessions, session stats |
|
|
42
|
+
| **Flagship RCA** | `login_failure_rca` — brute-force (spray/targeted) vs misconfigured client vs expired-credential storm vs lockout storm; `stale_access_audit` — dormant users, never-logged-in accounts, service accounts used interactively, orphaned sessions; `client_misconfig_audit` — wildcard/http redirect URIs, public clients with secrets, implicit flow, missing PKCE, password grant (ranked risk); `mfa_coverage_analysis` — coverage %, gap list |
|
|
43
|
+
| Governed writes | `disable_user` / `enable_user` (undo pair), `revoke_user_sessions`, `require_password_reset` (undo clears the flag), `update_client_redirect_uris` (undo replays the prior list), `rotate_client_secret` (masked priorState) |
|
|
44
|
+
|
|
45
|
+
27 MCP tools: 21 reads (including the 4 analyses) + 6 governed writes. The
|
|
46
|
+
same tools work on both platforms — a per-target `platform` field selects the
|
|
47
|
+
API shape (auth flow + resource paths).
|
|
48
|
+
|
|
49
|
+
## Supported platforms
|
|
50
|
+
|
|
51
|
+
| Platform | API | Auth |
|
|
52
|
+
|----------|-----|------|
|
|
53
|
+
| **keycloak** | Admin REST API (`/admin/realms/{realm}/...`) | OAuth2 client-credentials grant against `/realms/{realm}/protocol/openid-connect/token`; short-lived token auto-refreshed on 401 |
|
|
54
|
+
| **authentik** | API v3 (`/api/v3/...`) | Long-lived API token (Bearer) |
|
|
55
|
+
|
|
56
|
+
Platform notes: `require_password_reset`, `rotate_client_secret`,
|
|
57
|
+
`client_sessions`, `client_session_stats`, and `user_lockout_status` are
|
|
58
|
+
Keycloak-shaped (authentik has no equivalent endpoint — the tools return a
|
|
59
|
+
teaching error there). authentik contributes the global session list used by
|
|
60
|
+
the stale-access audit's orphaned-session check.
|
|
61
|
+
|
|
62
|
+
Missing a platform (Authelia, Zitadel, Ory...), an endpoint, or an analysis
|
|
63
|
+
you need? **缺功能提 issue/PR 欢迎留言** — open an issue or PR at
|
|
64
|
+
https://github.com/AIops-tools/Identity-AIops, feature requests welcome.
|
|
65
|
+
|
|
66
|
+
## Quick start
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv tool install identity-aiops # or: pipx install identity-aiops
|
|
70
|
+
|
|
71
|
+
identity-aiops init # wizard: target + realm + credential (encrypted) + policy seed
|
|
72
|
+
identity-aiops doctor # token acquisition + user-count probe per target
|
|
73
|
+
identity-aiops overview # one-shot estate summary
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For Keycloak, create a **confidential client** with *Service accounts roles*
|
|
77
|
+
enabled and grant its service account the realm-management roles you want the
|
|
78
|
+
agent to have (start with `view-users`, `view-events`, `view-clients`; add
|
|
79
|
+
`manage-users` / `manage-clients` only if you want the governed writes). For
|
|
80
|
+
authentik, create an API token for a (least-privileged) admin user.
|
|
81
|
+
|
|
82
|
+
CLI surface: `users` (list/show/sessions/credentials + disable/enable/
|
|
83
|
+
revoke-sessions/require-reset), `clients` (list/show + set-redirect-uris/
|
|
84
|
+
rotate-secret), `events`, `overview`, `doctor`, `secret`, `init`, `mcp`.
|
|
85
|
+
Write commands take `--dry-run` and always double-confirm; execution is
|
|
86
|
+
delegated to the governed MCP twins so CLI writes land in the audit log too.
|
|
87
|
+
|
|
88
|
+
## MCP configuration
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"identity-aiops": {
|
|
94
|
+
"command": "uvx",
|
|
95
|
+
"args": ["--from", "identity-aiops", "identity-aiops-mcp"],
|
|
96
|
+
"env": {
|
|
97
|
+
"IDENTITY_AIOPS_MASTER_PASSWORD": "<master password for secrets.enc>"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> **env-block caveat**: MCP clients launch the server with a minimal
|
|
105
|
+
> environment — variables from your shell profile are NOT inherited. Anything
|
|
106
|
+
> the server needs (`IDENTITY_AIOPS_MASTER_PASSWORD`, an alternate
|
|
107
|
+
> `IDENTITY_AIOPS_HOME`, `IDENTITY_AUDIT_APPROVED_BY` for high-risk writes)
|
|
108
|
+
> must be set in the `env` block above.
|
|
109
|
+
|
|
110
|
+
## Governance
|
|
111
|
+
|
|
112
|
+
Every MCP tool runs through the vendored governance harness
|
|
113
|
+
(`identity_aiops/governance/`, zero external dependencies):
|
|
114
|
+
|
|
115
|
+
- **Audit** — every call (including denials and errors) lands in
|
|
116
|
+
`~/.identity-aiops/audit.db` with params, status, risk level, and approver.
|
|
117
|
+
- **Budget** — per-session call/time budgets and a runaway breaker
|
|
118
|
+
(`IDENTITY_MAX_TOOL_CALLS`, `IDENTITY_MAX_TOOL_SECONDS`,
|
|
119
|
+
`IDENTITY_RUNAWAY_MAX`).
|
|
120
|
+
- **Risk tiers + approval** — reads are `low`; containment/hygiene writes
|
|
121
|
+
(`disable_user`, `revoke_user_sessions`, `require_password_reset`) are
|
|
122
|
+
`medium`; access-granting or boundary-replacing writes (`enable_user`,
|
|
123
|
+
`update_client_redirect_uris`, `rotate_client_secret`) are `high`.
|
|
124
|
+
**Secure by default**: with no `rules.yaml`, high/critical writes are denied
|
|
125
|
+
unless a named approver is present (`IDENTITY_AUDIT_APPROVED_BY`, plus
|
|
126
|
+
`IDENTITY_AUDIT_RATIONALE`). `identity-aiops init` seeds this dual-control
|
|
127
|
+
rule explicitly; the file hot-reloads.
|
|
128
|
+
- **Undo** — reversible writes capture the REAL prior state (fetched before
|
|
129
|
+
mutating, never guessed) and record a replayable inverse descriptor in
|
|
130
|
+
`~/.identity-aiops/undo.db`. Irreversible writes (`revoke_user_sessions`,
|
|
131
|
+
`rotate_client_secret`) record priorState only — the secret is stored
|
|
132
|
+
**masked**, never in clear.
|
|
133
|
+
- **Sanitize** — every string an IdP returns is folded through an
|
|
134
|
+
injection-safe normaliser (bounded length, control characters stripped)
|
|
135
|
+
before an agent sees it.
|
|
136
|
+
- **Secrets** — credentials live Fernet-encrypted (scrypt-derived key) in
|
|
137
|
+
`~/.identity-aiops/secrets.enc`, never plaintext on disk; a legacy
|
|
138
|
+
`IDENTITY_<TARGET>_SECRET` env var is honoured as fallback and
|
|
139
|
+
`identity-aiops secret migrate` moves it in. TLS verification defaults ON.
|
|
140
|
+
|
|
141
|
+
## Configuration
|
|
142
|
+
|
|
143
|
+
`~/.identity-aiops/config.yaml` (created by `init`):
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
targets:
|
|
147
|
+
- name: sso1
|
|
148
|
+
platform: keycloak
|
|
149
|
+
base_url: https://sso.example.com
|
|
150
|
+
realm: master
|
|
151
|
+
username: identity-aiops-agent # the confidential client's client_id
|
|
152
|
+
verify_ssl: true
|
|
153
|
+
- name: ak1
|
|
154
|
+
platform: authentik
|
|
155
|
+
base_url: https://auth.example.com
|
|
156
|
+
verify_ssl: true
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Set `IDENTITY_AIOPS_HOME` to relocate all state (config, secrets, audit,
|
|
160
|
+
undo, policy). `IDENTITY_AIOPS_CONFIG` points the MCP server at an alternate
|
|
161
|
+
config file.
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
uv sync
|
|
167
|
+
uv run pytest -q
|
|
168
|
+
uv run ruff check .
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
MIT License. Part of the [AIops-tools](https://github.com/AIops-tools) line —
|
|
172
|
+
governed AI-ops tooling for self-hosted infrastructure.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Identity AIops
|
|
2
|
+
|
|
3
|
+
**Governed AI-ops for self-hosted identity providers — Keycloak + authentik.**
|
|
4
|
+
|
|
5
|
+
`identity-aiops` gives AI agents (and the humans supervising them) a safe,
|
|
6
|
+
audited way to operate the identity plane of self-built and small/mid-size
|
|
7
|
+
infrastructure: who can sign in, what failed and why, which OAuth clients are
|
|
8
|
+
misconfigured, and who still has no second factor. It is built for teams
|
|
9
|
+
running their own Keycloak or authentik who want agent-driven identity
|
|
10
|
+
operations **with receipts** — every tool call is audited, writes carry risk
|
|
11
|
+
tiers with dry-run previews and undo tokens, and high-risk actions require a
|
|
12
|
+
named human approver.
|
|
13
|
+
|
|
14
|
+
> **Preview**: modelled on the public Keycloak admin REST API and authentik API
|
|
15
|
+
> v3 and exercised against mocked responses; not yet validated against live
|
|
16
|
+
> production instances. `identity-aiops doctor` is the fastest live check.
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
| Area | Tools |
|
|
21
|
+
|------|-------|
|
|
22
|
+
| Realm / system | overview, realm settings (brute-force protection, password/OTP policy), identity providers |
|
|
23
|
+
| Users | list/search, detail, count, sessions, credentials (MFA surface), groups, members, lockout status |
|
|
24
|
+
| Events | authentication events, admin/config-change events |
|
|
25
|
+
| Clients | list, detail, per-client sessions, session stats |
|
|
26
|
+
| **Flagship RCA** | `login_failure_rca` — brute-force (spray/targeted) vs misconfigured client vs expired-credential storm vs lockout storm; `stale_access_audit` — dormant users, never-logged-in accounts, service accounts used interactively, orphaned sessions; `client_misconfig_audit` — wildcard/http redirect URIs, public clients with secrets, implicit flow, missing PKCE, password grant (ranked risk); `mfa_coverage_analysis` — coverage %, gap list |
|
|
27
|
+
| Governed writes | `disable_user` / `enable_user` (undo pair), `revoke_user_sessions`, `require_password_reset` (undo clears the flag), `update_client_redirect_uris` (undo replays the prior list), `rotate_client_secret` (masked priorState) |
|
|
28
|
+
|
|
29
|
+
27 MCP tools: 21 reads (including the 4 analyses) + 6 governed writes. The
|
|
30
|
+
same tools work on both platforms — a per-target `platform` field selects the
|
|
31
|
+
API shape (auth flow + resource paths).
|
|
32
|
+
|
|
33
|
+
## Supported platforms
|
|
34
|
+
|
|
35
|
+
| Platform | API | Auth |
|
|
36
|
+
|----------|-----|------|
|
|
37
|
+
| **keycloak** | Admin REST API (`/admin/realms/{realm}/...`) | OAuth2 client-credentials grant against `/realms/{realm}/protocol/openid-connect/token`; short-lived token auto-refreshed on 401 |
|
|
38
|
+
| **authentik** | API v3 (`/api/v3/...`) | Long-lived API token (Bearer) |
|
|
39
|
+
|
|
40
|
+
Platform notes: `require_password_reset`, `rotate_client_secret`,
|
|
41
|
+
`client_sessions`, `client_session_stats`, and `user_lockout_status` are
|
|
42
|
+
Keycloak-shaped (authentik has no equivalent endpoint — the tools return a
|
|
43
|
+
teaching error there). authentik contributes the global session list used by
|
|
44
|
+
the stale-access audit's orphaned-session check.
|
|
45
|
+
|
|
46
|
+
Missing a platform (Authelia, Zitadel, Ory...), an endpoint, or an analysis
|
|
47
|
+
you need? **缺功能提 issue/PR 欢迎留言** — open an issue or PR at
|
|
48
|
+
https://github.com/AIops-tools/Identity-AIops, feature requests welcome.
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv tool install identity-aiops # or: pipx install identity-aiops
|
|
54
|
+
|
|
55
|
+
identity-aiops init # wizard: target + realm + credential (encrypted) + policy seed
|
|
56
|
+
identity-aiops doctor # token acquisition + user-count probe per target
|
|
57
|
+
identity-aiops overview # one-shot estate summary
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For Keycloak, create a **confidential client** with *Service accounts roles*
|
|
61
|
+
enabled and grant its service account the realm-management roles you want the
|
|
62
|
+
agent to have (start with `view-users`, `view-events`, `view-clients`; add
|
|
63
|
+
`manage-users` / `manage-clients` only if you want the governed writes). For
|
|
64
|
+
authentik, create an API token for a (least-privileged) admin user.
|
|
65
|
+
|
|
66
|
+
CLI surface: `users` (list/show/sessions/credentials + disable/enable/
|
|
67
|
+
revoke-sessions/require-reset), `clients` (list/show + set-redirect-uris/
|
|
68
|
+
rotate-secret), `events`, `overview`, `doctor`, `secret`, `init`, `mcp`.
|
|
69
|
+
Write commands take `--dry-run` and always double-confirm; execution is
|
|
70
|
+
delegated to the governed MCP twins so CLI writes land in the audit log too.
|
|
71
|
+
|
|
72
|
+
## MCP configuration
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"identity-aiops": {
|
|
78
|
+
"command": "uvx",
|
|
79
|
+
"args": ["--from", "identity-aiops", "identity-aiops-mcp"],
|
|
80
|
+
"env": {
|
|
81
|
+
"IDENTITY_AIOPS_MASTER_PASSWORD": "<master password for secrets.enc>"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> **env-block caveat**: MCP clients launch the server with a minimal
|
|
89
|
+
> environment — variables from your shell profile are NOT inherited. Anything
|
|
90
|
+
> the server needs (`IDENTITY_AIOPS_MASTER_PASSWORD`, an alternate
|
|
91
|
+
> `IDENTITY_AIOPS_HOME`, `IDENTITY_AUDIT_APPROVED_BY` for high-risk writes)
|
|
92
|
+
> must be set in the `env` block above.
|
|
93
|
+
|
|
94
|
+
## Governance
|
|
95
|
+
|
|
96
|
+
Every MCP tool runs through the vendored governance harness
|
|
97
|
+
(`identity_aiops/governance/`, zero external dependencies):
|
|
98
|
+
|
|
99
|
+
- **Audit** — every call (including denials and errors) lands in
|
|
100
|
+
`~/.identity-aiops/audit.db` with params, status, risk level, and approver.
|
|
101
|
+
- **Budget** — per-session call/time budgets and a runaway breaker
|
|
102
|
+
(`IDENTITY_MAX_TOOL_CALLS`, `IDENTITY_MAX_TOOL_SECONDS`,
|
|
103
|
+
`IDENTITY_RUNAWAY_MAX`).
|
|
104
|
+
- **Risk tiers + approval** — reads are `low`; containment/hygiene writes
|
|
105
|
+
(`disable_user`, `revoke_user_sessions`, `require_password_reset`) are
|
|
106
|
+
`medium`; access-granting or boundary-replacing writes (`enable_user`,
|
|
107
|
+
`update_client_redirect_uris`, `rotate_client_secret`) are `high`.
|
|
108
|
+
**Secure by default**: with no `rules.yaml`, high/critical writes are denied
|
|
109
|
+
unless a named approver is present (`IDENTITY_AUDIT_APPROVED_BY`, plus
|
|
110
|
+
`IDENTITY_AUDIT_RATIONALE`). `identity-aiops init` seeds this dual-control
|
|
111
|
+
rule explicitly; the file hot-reloads.
|
|
112
|
+
- **Undo** — reversible writes capture the REAL prior state (fetched before
|
|
113
|
+
mutating, never guessed) and record a replayable inverse descriptor in
|
|
114
|
+
`~/.identity-aiops/undo.db`. Irreversible writes (`revoke_user_sessions`,
|
|
115
|
+
`rotate_client_secret`) record priorState only — the secret is stored
|
|
116
|
+
**masked**, never in clear.
|
|
117
|
+
- **Sanitize** — every string an IdP returns is folded through an
|
|
118
|
+
injection-safe normaliser (bounded length, control characters stripped)
|
|
119
|
+
before an agent sees it.
|
|
120
|
+
- **Secrets** — credentials live Fernet-encrypted (scrypt-derived key) in
|
|
121
|
+
`~/.identity-aiops/secrets.enc`, never plaintext on disk; a legacy
|
|
122
|
+
`IDENTITY_<TARGET>_SECRET` env var is honoured as fallback and
|
|
123
|
+
`identity-aiops secret migrate` moves it in. TLS verification defaults ON.
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
`~/.identity-aiops/config.yaml` (created by `init`):
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
targets:
|
|
131
|
+
- name: sso1
|
|
132
|
+
platform: keycloak
|
|
133
|
+
base_url: https://sso.example.com
|
|
134
|
+
realm: master
|
|
135
|
+
username: identity-aiops-agent # the confidential client's client_id
|
|
136
|
+
verify_ssl: true
|
|
137
|
+
- name: ak1
|
|
138
|
+
platform: authentik
|
|
139
|
+
base_url: https://auth.example.com
|
|
140
|
+
verify_ssl: true
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Set `IDENTITY_AIOPS_HOME` to relocate all state (config, secrets, audit,
|
|
144
|
+
undo, policy). `IDENTITY_AIOPS_CONFIG` points the MCP server at an alternate
|
|
145
|
+
config file.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
uv sync
|
|
151
|
+
uv run pytest -q
|
|
152
|
+
uv run ruff check .
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
MIT License. Part of the [AIops-tools](https://github.com/AIops-tools) line —
|
|
156
|
+
governed AI-ops tooling for self-hosted infrastructure.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Identity AIops v0.1.0 — preview
|
|
2
|
+
|
|
3
|
+
Governed AI-ops for **Keycloak** and **authentik** identity providers for AI
|
|
4
|
+
agents, with a built-in governance harness (audit, policy, token/runaway
|
|
5
|
+
budget, undo-token recording, graduated risk tiers) and an encrypted credential
|
|
6
|
+
store. Standalone — no external skill-family dependency. One MCP server spans
|
|
7
|
+
both platforms: a per-target `platform` field selects the API shape, and the
|
|
8
|
+
same 27 tools work on Keycloak (admin REST `/admin/realms/{realm}/...`,
|
|
9
|
+
client-credentials grant with automatic refresh-on-401) and authentik (API v3
|
|
10
|
+
`/api/v3/...`, Bearer token).
|
|
11
|
+
|
|
12
|
+
> **Not affiliated with, endorsed by, or sponsored by the Keycloak project,
|
|
13
|
+
> Red Hat, Authentik Security Inc., or the authentik project.** Keycloak and
|
|
14
|
+
> authentik are trademarks of their respective owners.
|
|
15
|
+
|
|
16
|
+
> **Preview / mock-only.** All behaviour is validated against mocked
|
|
17
|
+
> Keycloak/authentik JSON responses; it has **not** been run against a live
|
|
18
|
+
> IdP. The concrete REST paths are modelled from each project's public API and
|
|
19
|
+
> need live verification. Both platforms are free/self-hostable, so a self-hosted
|
|
20
|
+
> lab is the easiest live check — `identity-aiops doctor` is the fastest.
|
|
21
|
+
|
|
22
|
+
## Highlights
|
|
23
|
+
|
|
24
|
+
- **27 MCP tools** (21 read, 6 write), every one wrapped with `@governed_tool`:
|
|
25
|
+
- **Realm / system** — `identity_overview`, `realm_info`,
|
|
26
|
+
`list_identity_providers`.
|
|
27
|
+
- **Users / groups** — `list_users`, `user_detail`, `user_count`,
|
|
28
|
+
`user_sessions`, `user_credentials`, `list_groups`, `group_members`,
|
|
29
|
+
`user_lockout_status`.
|
|
30
|
+
- **Events** — `login_events`, `admin_events`.
|
|
31
|
+
- **Clients** — `list_clients`, `client_detail`, `client_sessions`,
|
|
32
|
+
`client_session_stats`.
|
|
33
|
+
- **Writes** — `disable_user`, `revoke_user_sessions`,
|
|
34
|
+
`require_password_reset` (med); `enable_user`,
|
|
35
|
+
`update_client_redirect_uris`, `rotate_client_secret` (**high**).
|
|
36
|
+
- **Flagship analyses** (transparent heuristics that show their numbers):
|
|
37
|
+
- `login_failure_rca` — failed-auth events windowed by user/IP/client:
|
|
38
|
+
password spray, targeted brute-force, stale stored credential,
|
|
39
|
+
misconfigured client (rotated secret still deployed), expired-credential
|
|
40
|
+
storm, lockout storm — each with cause + action.
|
|
41
|
+
- `stale_access_audit` — enabled users idle > N days, never-logged-in
|
|
42
|
+
accounts, service accounts used interactively, orphaned sessions.
|
|
43
|
+
- `client_misconfig_audit` — wildcard/plain-http redirect URIs, public
|
|
44
|
+
clients carrying secrets, implicit flow, missing PKCE, password grant —
|
|
45
|
+
ranked per-client riskScore with evidence.
|
|
46
|
+
- `mfa_coverage_analysis` — coverage %, per-group breakdown, gap list.
|
|
47
|
+
- **Governed writes** — reversible writes capture the **real fetched
|
|
48
|
+
before-state** and record an undo descriptor (`disable_user` ↔
|
|
49
|
+
`enable_user`; `require_password_reset` undoes via `clear=True`;
|
|
50
|
+
`update_client_redirect_uris` replays the prior list). Irreversible writes
|
|
51
|
+
record priorState only — `rotate_client_secret` stores a **masked**
|
|
52
|
+
fingerprint, never the value. High-risk writes take a `dry_run` preview and
|
|
53
|
+
require an approver.
|
|
54
|
+
- **Encrypted secret store** — the Keycloak client secret or authentik API
|
|
55
|
+
token lives encrypted in `~/.identity-aiops/secrets.enc` (Fernet + scrypt),
|
|
56
|
+
never plaintext; legacy `IDENTITY_<TARGET>_SECRET` env fallback. TLS
|
|
57
|
+
verification defaults ON.
|
|
58
|
+
- **Secure by default** — with no `rules.yaml`, high-risk writes are denied
|
|
59
|
+
unless `IDENTITY_AUDIT_APPROVED_BY` names an approver; `init` seeds the
|
|
60
|
+
dual-control rule explicitly.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv tool install identity-aiops
|
|
66
|
+
identity-aiops init
|
|
67
|
+
identity-aiops doctor
|
|
68
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Disclaimer
|
|
4
|
+
|
|
5
|
+
Community-maintained open-source project. **Not affiliated with, endorsed by, or
|
|
6
|
+
sponsored by the Keycloak project, Red Hat, Authentik Security Inc., or the
|
|
7
|
+
authentik project.** Product and trademark names (Keycloak, authentik) belong to
|
|
8
|
+
their owners. Source is auditable under the MIT license.
|
|
9
|
+
|
|
10
|
+
## Reporting Vulnerabilities
|
|
11
|
+
|
|
12
|
+
Report privately via a GitHub Security Advisory on
|
|
13
|
+
[github.com/AIops-tools/Identity-AIops](https://github.com/AIops-tools/Identity-AIops/security/advisories)
|
|
14
|
+
or email zhouwei008@gmail.com. Please do not open public issues for security
|
|
15
|
+
reports.
|
|
16
|
+
|
|
17
|
+
## Security Design
|
|
18
|
+
|
|
19
|
+
### Credential Management
|
|
20
|
+
- Per-target secrets — the Keycloak confidential client's **client secret** or the
|
|
21
|
+
authentik **API token** — live **encrypted** in
|
|
22
|
+
`~/.identity-aiops/secrets.enc` (Fernet/AES-128 + scrypt-derived key; chmod
|
|
23
|
+
600), never in `config.yaml` and never in source. The master password is never
|
|
24
|
+
stored — only a per-store random salt and the ciphertext are on disk.
|
|
25
|
+
- A legacy plaintext env var `IDENTITY_<TARGET_NAME_UPPER>_SECRET` is still
|
|
26
|
+
honoured as a fallback with a deprecation warning (migrate with
|
|
27
|
+
`identity-aiops secret migrate`).
|
|
28
|
+
- The secret is held only in memory and never logged or echoed. Keycloak's client
|
|
29
|
+
secret is exchanged at request time for a short-lived access token
|
|
30
|
+
(client-credentials grant, refreshed once on a 401); authentik's API token is
|
|
31
|
+
presented as a Bearer header. The config file holds only platform, base URL,
|
|
32
|
+
realm, client_id, and TLS settings.
|
|
33
|
+
- `rotate_client_secret` never returns or records a secret in clear — old and new
|
|
34
|
+
values appear only as masked fingerprints in results and the audit log.
|
|
35
|
+
|
|
36
|
+
### Governed Operations
|
|
37
|
+
Every MCP tool runs through the bundled `@governed_tool` harness
|
|
38
|
+
(`identity_aiops.governance`):
|
|
39
|
+
- **Audit** — every call logged to a local SQLite DB under `~/.identity-aiops/`
|
|
40
|
+
(relocatable via `IDENTITY_AIOPS_HOME`), agent-attributed, secret-redacted.
|
|
41
|
+
- **Token/runaway budget** — hard ceilings (`IDENTITY_MAX_TOOL_CALLS` /
|
|
42
|
+
`IDENTITY_MAX_TOOL_SECONDS`) plus an on-by-default guard that trips a tight
|
|
43
|
+
poll/retry loop, preventing unbounded API consumption.
|
|
44
|
+
- **Graduated risk tiers** — `~/.identity-aiops/rules.yaml` `risk_tiers` gate
|
|
45
|
+
writes by environment/tag; the highest tiers require a recorded approver.
|
|
46
|
+
- **Undo-token recording** — reversible writes capture the BEFORE state (via a
|
|
47
|
+
real GET) and record an inverse descriptor (`disable_user` ↔ `enable_user`;
|
|
48
|
+
`require_password_reset` clears via its own `clear=True` path;
|
|
49
|
+
`update_client_redirect_uris` replays the prior list) so the change can be
|
|
50
|
+
rolled back.
|
|
51
|
+
|
|
52
|
+
### State-Changing Operations
|
|
53
|
+
Access-granting / boundary-replacing writes — `enable_user`,
|
|
54
|
+
`update_client_redirect_uris`, `rotate_client_secret` — are `risk_level=high`,
|
|
55
|
+
accept a `dry_run` preview, and (under `risk_tiers`) require a recorded approver
|
|
56
|
+
(`IDENTITY_AUDIT_APPROVED_BY` + `IDENTITY_AUDIT_RATIONALE`).
|
|
57
|
+
`revoke_user_sessions` and `rotate_client_secret` are irreversible (priorState
|
|
58
|
+
recorded, no undo). Containment/hygiene writes — `disable_user`,
|
|
59
|
+
`revoke_user_sessions`, `require_password_reset` — are `risk_level=medium`.
|
|
60
|
+
|
|
61
|
+
### SSL/TLS Verification
|
|
62
|
+
`verify_ssl` defaults to true; disable only for self-signed lab certificates.
|
|
63
|
+
|
|
64
|
+
### Prompt-Injection Protection
|
|
65
|
+
All IdP-returned text (usernames, email addresses, event messages, client names,
|
|
66
|
+
redirect URIs, group names) is passed through a `sanitize()` truncate +
|
|
67
|
+
control-character strip before reaching the agent.
|
|
68
|
+
|
|
69
|
+
### Network Scope
|
|
70
|
+
No webhooks, no telemetry, no outbound calls beyond the configured Keycloak /
|
|
71
|
+
authentik REST API endpoints. No post-install scripts or background services.
|
|
72
|
+
|
|
73
|
+
## Static Analysis
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uvx bandit -r identity_aiops/ mcp_server/
|
|
77
|
+
uv run ruff check .
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Supported Versions
|
|
81
|
+
|
|
82
|
+
The latest released version receives security fixes. This is a preview (0.x);
|
|
83
|
+
pin a version in production.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""identity-aiops — governed Keycloak + authentik identity operations for AI agents.
|
|
2
|
+
|
|
3
|
+
Standalone and self-contained: the governance harness (audit, token budget,
|
|
4
|
+
undo-token recording, graduated risk tiers, output sanitize) is
|
|
5
|
+
bundled under ``identity_aiops.governance`` — this package has no external
|
|
6
|
+
skill-family dependency. Preview: not yet full-coverage.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("identity-aiops")
|
|
13
|
+
except PackageNotFoundError: # running from an uninstalled source tree
|
|
14
|
+
__version__ = "0.0.0+unknown"
|