niitaka-sdk 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.
- niitaka_sdk-0.1.0/.gitignore +30 -0
- niitaka_sdk-0.1.0/PKG-INFO +114 -0
- niitaka_sdk-0.1.0/README_SDK.md +73 -0
- niitaka_sdk-0.1.0/niitaka/__init__.py +26 -0
- niitaka_sdk-0.1.0/niitaka/cli.py +278 -0
- niitaka_sdk-0.1.0/niitaka/client.py +1026 -0
- niitaka_sdk-0.1.0/niitaka/instrumentation.py +1070 -0
- niitaka_sdk-0.1.0/niitaka/langchain_handler.py +362 -0
- niitaka_sdk-0.1.0/niitaka/policy_engine.py +271 -0
- niitaka_sdk-0.1.0/niitaka/policy_loader.py +172 -0
- niitaka_sdk-0.1.0/niitaka/policy_schemas.py +283 -0
- niitaka_sdk-0.1.0/niitaka/pricing.py +199 -0
- niitaka_sdk-0.1.0/niitaka/testing.py +25 -0
- niitaka_sdk-0.1.0/pyproject.toml +56 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
|
|
9
|
+
# Environment / secrets — never commit these
|
|
10
|
+
.env
|
|
11
|
+
.env.local
|
|
12
|
+
.env.*.local
|
|
13
|
+
agents/.env
|
|
14
|
+
backend/.env
|
|
15
|
+
backend/.env.dev
|
|
16
|
+
backend/.env.prod
|
|
17
|
+
|
|
18
|
+
# Node / Next.js
|
|
19
|
+
node_modules/
|
|
20
|
+
.next/
|
|
21
|
+
*.tsbuildinfo
|
|
22
|
+
frontend/next-env.d.ts
|
|
23
|
+
frontend/package-lock.json
|
|
24
|
+
|
|
25
|
+
# macOS
|
|
26
|
+
.DS_Store
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: niitaka-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI agent observability and control plane SDK
|
|
5
|
+
Project-URL: Homepage, https://niitaka.ai
|
|
6
|
+
Project-URL: Docs, https://docs.niitaka.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/amigotomodachiami/Niitaka
|
|
8
|
+
Author-email: Niitaka <hello@niitaka.ai>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,ai,anthropic,gemini,groq,langchain,llm,observability,openai,tracing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.24
|
|
23
|
+
Requires-Dist: requests>=2.28
|
|
24
|
+
Provides-Extra: all
|
|
25
|
+
Requires-Dist: anthropic>=0.20; extra == 'all'
|
|
26
|
+
Requires-Dist: google-generativeai>=0.4; extra == 'all'
|
|
27
|
+
Requires-Dist: groq>=0.4; extra == 'all'
|
|
28
|
+
Requires-Dist: langchain-core>=0.1; extra == 'all'
|
|
29
|
+
Requires-Dist: openai>=1.0; extra == 'all'
|
|
30
|
+
Provides-Extra: anthropic
|
|
31
|
+
Requires-Dist: anthropic>=0.20; extra == 'anthropic'
|
|
32
|
+
Provides-Extra: gemini
|
|
33
|
+
Requires-Dist: google-generativeai>=0.4; extra == 'gemini'
|
|
34
|
+
Provides-Extra: groq
|
|
35
|
+
Requires-Dist: groq>=0.4; extra == 'groq'
|
|
36
|
+
Provides-Extra: langchain
|
|
37
|
+
Requires-Dist: langchain-core>=0.1; extra == 'langchain'
|
|
38
|
+
Provides-Extra: openai
|
|
39
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# Niitaka SDK
|
|
43
|
+
|
|
44
|
+
**AI agent observability and control plane — one import away.**
|
|
45
|
+
|
|
46
|
+
Niitaka instruments your AI agents to capture every LLM call, tool invocation, and decision trace. Connect to the [Niitaka dashboard](https://niitaka.ai) to run experiments, enforce guardrails, and version your agents — without changing your agent code.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install niitaka-sdk # core
|
|
52
|
+
pip install "niitaka-sdk[openai]" # + OpenAI
|
|
53
|
+
pip install "niitaka-sdk[anthropic]" # + Anthropic
|
|
54
|
+
pip install "niitaka-sdk[all]" # all providers
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import niitaka
|
|
61
|
+
import os
|
|
62
|
+
|
|
63
|
+
niitaka.configure(
|
|
64
|
+
api_key=os.getenv("NIITAKA_API_KEY"),
|
|
65
|
+
api_url="https://api.niitaka.ai", # or your self-hosted URL
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Instrument your LLM provider (pick one or more)
|
|
69
|
+
niitaka.instrument_openai()
|
|
70
|
+
niitaka.instrument_anthropic()
|
|
71
|
+
niitaka.instrument_gemini()
|
|
72
|
+
niitaka.instrument_groq()
|
|
73
|
+
|
|
74
|
+
# Wrap your agent run in a session
|
|
75
|
+
with niitaka.start_session(goal="Summarise this document", agent_id="my-agent"):
|
|
76
|
+
response = client.chat.completions.create(...) # auto-logged
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Every call inside `start_session` is automatically traced — latency, token usage, cost, errors, and tool calls.
|
|
80
|
+
|
|
81
|
+
## Providers
|
|
82
|
+
|
|
83
|
+
| Provider | Extra | Import |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| OpenAI | `niitaka-sdk[openai]` | `niitaka.instrument_openai()` |
|
|
86
|
+
| Anthropic | `niitaka-sdk[anthropic]` | `niitaka.instrument_anthropic()` |
|
|
87
|
+
| Google Gemini | `niitaka-sdk[gemini]` | `niitaka.instrument_gemini()` |
|
|
88
|
+
| Groq | `niitaka-sdk[groq]` | `niitaka.instrument_groq()` |
|
|
89
|
+
| LangChain | `niitaka-sdk[langchain]` | `from niitaka import NiitakaCallbackHandler` |
|
|
90
|
+
|
|
91
|
+
## LangChain
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from niitaka import NiitakaCallbackHandler
|
|
95
|
+
import niitaka
|
|
96
|
+
|
|
97
|
+
with niitaka.start_session(goal="...", agent_id="lc-agent"):
|
|
98
|
+
chain.invoke({"input": "..."}, config={"callbacks": [NiitakaCallbackHandler()]})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Runtime config
|
|
102
|
+
|
|
103
|
+
Niitaka can push model, temperature, and guardrail config to your agent at runtime — no redeploy needed:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
config = niitaka.get_runtime_config(agent_id="my-agent")
|
|
107
|
+
# config["llm"]["model"], config["guardrails"]["cost_limit_usd"], ...
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Links
|
|
111
|
+
|
|
112
|
+
- **Dashboard**: [niitaka.ai](https://niitaka.ai)
|
|
113
|
+
- **Docs**: [docs.niitaka.ai](https://docs.niitaka.ai)
|
|
114
|
+
- **Support**: hello@niitaka.ai
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Niitaka SDK
|
|
2
|
+
|
|
3
|
+
**AI agent observability and control plane — one import away.**
|
|
4
|
+
|
|
5
|
+
Niitaka instruments your AI agents to capture every LLM call, tool invocation, and decision trace. Connect to the [Niitaka dashboard](https://niitaka.ai) to run experiments, enforce guardrails, and version your agents — without changing your agent code.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install niitaka-sdk # core
|
|
11
|
+
pip install "niitaka-sdk[openai]" # + OpenAI
|
|
12
|
+
pip install "niitaka-sdk[anthropic]" # + Anthropic
|
|
13
|
+
pip install "niitaka-sdk[all]" # all providers
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import niitaka
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
niitaka.configure(
|
|
23
|
+
api_key=os.getenv("NIITAKA_API_KEY"),
|
|
24
|
+
api_url="https://api.niitaka.ai", # or your self-hosted URL
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Instrument your LLM provider (pick one or more)
|
|
28
|
+
niitaka.instrument_openai()
|
|
29
|
+
niitaka.instrument_anthropic()
|
|
30
|
+
niitaka.instrument_gemini()
|
|
31
|
+
niitaka.instrument_groq()
|
|
32
|
+
|
|
33
|
+
# Wrap your agent run in a session
|
|
34
|
+
with niitaka.start_session(goal="Summarise this document", agent_id="my-agent"):
|
|
35
|
+
response = client.chat.completions.create(...) # auto-logged
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Every call inside `start_session` is automatically traced — latency, token usage, cost, errors, and tool calls.
|
|
39
|
+
|
|
40
|
+
## Providers
|
|
41
|
+
|
|
42
|
+
| Provider | Extra | Import |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| OpenAI | `niitaka-sdk[openai]` | `niitaka.instrument_openai()` |
|
|
45
|
+
| Anthropic | `niitaka-sdk[anthropic]` | `niitaka.instrument_anthropic()` |
|
|
46
|
+
| Google Gemini | `niitaka-sdk[gemini]` | `niitaka.instrument_gemini()` |
|
|
47
|
+
| Groq | `niitaka-sdk[groq]` | `niitaka.instrument_groq()` |
|
|
48
|
+
| LangChain | `niitaka-sdk[langchain]` | `from niitaka import NiitakaCallbackHandler` |
|
|
49
|
+
|
|
50
|
+
## LangChain
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from niitaka import NiitakaCallbackHandler
|
|
54
|
+
import niitaka
|
|
55
|
+
|
|
56
|
+
with niitaka.start_session(goal="...", agent_id="lc-agent"):
|
|
57
|
+
chain.invoke({"input": "..."}, config={"callbacks": [NiitakaCallbackHandler()]})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Runtime config
|
|
61
|
+
|
|
62
|
+
Niitaka can push model, temperature, and guardrail config to your agent at runtime — no redeploy needed:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
config = niitaka.get_runtime_config(agent_id="my-agent")
|
|
66
|
+
# config["llm"]["model"], config["guardrails"]["cost_limit_usd"], ...
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Links
|
|
70
|
+
|
|
71
|
+
- **Dashboard**: [niitaka.ai](https://niitaka.ai)
|
|
72
|
+
- **Docs**: [docs.niitaka.ai](https://docs.niitaka.ai)
|
|
73
|
+
- **Support**: hello@niitaka.ai
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .instrumentation import (
|
|
2
|
+
instrument_openai,
|
|
3
|
+
instrument_anthropic,
|
|
4
|
+
instrument_gemini,
|
|
5
|
+
instrument_groq,
|
|
6
|
+
)
|
|
7
|
+
from .client import get_runtime_config, start_session, configure
|
|
8
|
+
|
|
9
|
+
# NiitakaCallbackHandler is optional — only available when langchain-core is installed.
|
|
10
|
+
# Import it directly: from sdk.langchain_handler import NiitakaCallbackHandler
|
|
11
|
+
try:
|
|
12
|
+
from .langchain_handler import NiitakaCallbackHandler
|
|
13
|
+
except ImportError:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def auto_instrument():
|
|
18
|
+
"""Instrument all installed LLM providers.
|
|
19
|
+
|
|
20
|
+
Calls each provider's instrument function and silently skips any that
|
|
21
|
+
are not installed. Equivalent to calling each function individually.
|
|
22
|
+
"""
|
|
23
|
+
instrument_openai()
|
|
24
|
+
instrument_anthropic()
|
|
25
|
+
instrument_gemini()
|
|
26
|
+
instrument_groq()
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Niitaka CLI — manage policy files.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m sdk.cli push <policy-file> [--url URL] [--key KEY] [--dry-run]
|
|
5
|
+
python -m sdk.cli pull [--agent AGENT_ID] [--url URL] [--key KEY] [-o FILE]
|
|
6
|
+
python -m sdk.cli validate <policy-file>
|
|
7
|
+
python -m sdk.cli history [--agent AGENT_ID] [--limit N] [--url URL] [--key KEY]
|
|
8
|
+
|
|
9
|
+
Commands
|
|
10
|
+
--------
|
|
11
|
+
push Upload policies from a YAML/JSON file to the backend DB.
|
|
12
|
+
By default replaces all existing policies for each agent in the file.
|
|
13
|
+
|
|
14
|
+
pull Download current DB policies for one or all agents and print as YAML.
|
|
15
|
+
Use -o to write to a file instead of stdout.
|
|
16
|
+
|
|
17
|
+
validate Parse and validate a policy file without contacting the backend.
|
|
18
|
+
|
|
19
|
+
history Show the policy audit log — who changed what and when.
|
|
20
|
+
Use --agent to filter to a specific agent.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
import requests
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
def _headers(key: str) -> dict:
|
|
35
|
+
h = {"Content-Type": "application/json"}
|
|
36
|
+
if key:
|
|
37
|
+
h["X-API-Key"] = key
|
|
38
|
+
return h
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _base_url(url: str) -> str:
|
|
42
|
+
return url.rstrip("/")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── Commands ──────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
def cmd_validate(args: argparse.Namespace) -> int:
|
|
48
|
+
from sdk.policy_loader import load_policy_file, PolicyFileError
|
|
49
|
+
try:
|
|
50
|
+
policies = load_policy_file(args.file)
|
|
51
|
+
except PolicyFileError as exc:
|
|
52
|
+
print(f"[ERROR] {exc}", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
from collections import Counter
|
|
56
|
+
counts = Counter(p["agent_id"] for p in policies)
|
|
57
|
+
print(f"✓ Valid policy file — {len(policies)} policies across {len(counts)} agent(s):")
|
|
58
|
+
for agent_id, n in sorted(counts.items()):
|
|
59
|
+
print(f" {agent_id}: {n} rule(s)")
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cmd_push(args: argparse.Namespace) -> int:
|
|
64
|
+
from sdk.policy_loader import load_policy_file, group_by_agent, PolicyFileError
|
|
65
|
+
|
|
66
|
+
# Validate file first
|
|
67
|
+
try:
|
|
68
|
+
policies = load_policy_file(args.file)
|
|
69
|
+
except PolicyFileError as exc:
|
|
70
|
+
print(f"[ERROR] {exc}", file=sys.stderr)
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
grouped = group_by_agent(policies)
|
|
74
|
+
base = _base_url(args.url)
|
|
75
|
+
headers = _headers(args.key)
|
|
76
|
+
|
|
77
|
+
print(f"[niitaka push] {args.file} → {base}")
|
|
78
|
+
print(f" Agents: {', '.join(sorted(grouped))}")
|
|
79
|
+
|
|
80
|
+
if args.dry_run:
|
|
81
|
+
print(" [dry-run] No changes made.")
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
# Build import payload: list of policy objects with agent_id + config
|
|
85
|
+
payload_policies = []
|
|
86
|
+
for p in policies:
|
|
87
|
+
payload_policies.append({
|
|
88
|
+
"agent_id": p["agent_id"],
|
|
89
|
+
"type": p["type"],
|
|
90
|
+
"config": {
|
|
91
|
+
"condition": p["condition"],
|
|
92
|
+
"action": p["action"],
|
|
93
|
+
"priority": p["priority"],
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
resp = requests.post(
|
|
99
|
+
f"{base}/policies/import",
|
|
100
|
+
headers=headers,
|
|
101
|
+
json={"policies": payload_policies, "replace": True},
|
|
102
|
+
timeout=15,
|
|
103
|
+
)
|
|
104
|
+
resp.raise_for_status()
|
|
105
|
+
except requests.HTTPError as exc:
|
|
106
|
+
print(f"[ERROR] Backend returned {exc.response.status_code}: {exc.response.text}",
|
|
107
|
+
file=sys.stderr)
|
|
108
|
+
return 1
|
|
109
|
+
except requests.RequestException as exc:
|
|
110
|
+
print(f"[ERROR] Could not reach backend at {base}: {exc}", file=sys.stderr)
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
result = resp.json()
|
|
114
|
+
print(f" ✓ Imported {result.get('imported', '?')} policies "
|
|
115
|
+
f"(replaced {result.get('deleted', '?')} existing)")
|
|
116
|
+
for agent_id, n in sorted(result.get("by_agent", {}).items()):
|
|
117
|
+
print(f" {agent_id}: {n} rule(s) written")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def cmd_pull(args: argparse.Namespace) -> int:
|
|
122
|
+
from sdk.policy_loader import to_yaml_string
|
|
123
|
+
|
|
124
|
+
base = _base_url(args.url)
|
|
125
|
+
headers = _headers(args.key)
|
|
126
|
+
|
|
127
|
+
if args.agent:
|
|
128
|
+
# Single agent
|
|
129
|
+
try:
|
|
130
|
+
resp = requests.get(f"{base}/policies/{args.agent}", headers=headers, timeout=10)
|
|
131
|
+
resp.raise_for_status()
|
|
132
|
+
except requests.RequestException as exc:
|
|
133
|
+
print(f"[ERROR] {exc}", file=sys.stderr)
|
|
134
|
+
return 1
|
|
135
|
+
raw = resp.json()
|
|
136
|
+
# Attach agent_id (not in per-agent response)
|
|
137
|
+
for p in raw:
|
|
138
|
+
p["agent_id"] = args.agent
|
|
139
|
+
else:
|
|
140
|
+
# All policies
|
|
141
|
+
try:
|
|
142
|
+
resp = requests.get(f"{base}/policies", headers=headers, timeout=10)
|
|
143
|
+
resp.raise_for_status()
|
|
144
|
+
except requests.RequestException as exc:
|
|
145
|
+
print(f"[ERROR] {exc}", file=sys.stderr)
|
|
146
|
+
return 1
|
|
147
|
+
raw = resp.json()
|
|
148
|
+
|
|
149
|
+
if not raw:
|
|
150
|
+
print("# No policies found.", file=sys.stderr)
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
yaml_text = to_yaml_string(raw)
|
|
154
|
+
|
|
155
|
+
if args.output:
|
|
156
|
+
with open(args.output, "w", encoding="utf-8") as fh:
|
|
157
|
+
fh.write(yaml_text)
|
|
158
|
+
print(f"[niitaka pull] Wrote {len(raw)} policies to {args.output}")
|
|
159
|
+
else:
|
|
160
|
+
sys.stdout.write(yaml_text)
|
|
161
|
+
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cmd_history(args: argparse.Namespace) -> int:
|
|
166
|
+
base = _base_url(args.url)
|
|
167
|
+
headers = _headers(args.key)
|
|
168
|
+
|
|
169
|
+
url = f"{base}/policies/audit"
|
|
170
|
+
if args.agent:
|
|
171
|
+
url = f"{base}/policies/audit/{args.agent}"
|
|
172
|
+
url += f"?limit={args.limit}"
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
resp = requests.get(url, headers=headers, timeout=10)
|
|
176
|
+
resp.raise_for_status()
|
|
177
|
+
except requests.RequestException as exc:
|
|
178
|
+
print(f"[ERROR] {exc}", file=sys.stderr)
|
|
179
|
+
return 1
|
|
180
|
+
|
|
181
|
+
entries = resp.json()
|
|
182
|
+
if not entries:
|
|
183
|
+
print("No audit log entries found.")
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
# Column widths
|
|
187
|
+
W_TIME = 20
|
|
188
|
+
W_AGENT = 20
|
|
189
|
+
W_ACTION = 8
|
|
190
|
+
W_TYPE = 12
|
|
191
|
+
W_SOURCE = 6
|
|
192
|
+
|
|
193
|
+
header = (
|
|
194
|
+
f"{'Timestamp':<{W_TIME}} "
|
|
195
|
+
f"{'Agent':<{W_AGENT}} "
|
|
196
|
+
f"{'Action':<{W_ACTION}} "
|
|
197
|
+
f"{'Type':<{W_TYPE}} "
|
|
198
|
+
f"{'Src':<{W_SOURCE}}"
|
|
199
|
+
)
|
|
200
|
+
print(header)
|
|
201
|
+
print("─" * len(header))
|
|
202
|
+
|
|
203
|
+
for e in entries:
|
|
204
|
+
ts = e.get("created_at", "")
|
|
205
|
+
# Trim to readable format: "2026-04-12T14:32:01"
|
|
206
|
+
if ts and "T" in ts:
|
|
207
|
+
ts = ts[:19].replace("T", " ")
|
|
208
|
+
|
|
209
|
+
action = e.get("action", "")
|
|
210
|
+
agent = (e.get("agent_id") or "")[:W_AGENT]
|
|
211
|
+
ptype = (e.get("policy_type") or "—")[:W_TYPE]
|
|
212
|
+
source = (e.get("source") or "")[:W_SOURCE]
|
|
213
|
+
|
|
214
|
+
print(
|
|
215
|
+
f"{ts:<{W_TIME}} "
|
|
216
|
+
f"{agent:<{W_AGENT}} "
|
|
217
|
+
f"{action:<{W_ACTION}} "
|
|
218
|
+
f"{ptype:<{W_TYPE}} "
|
|
219
|
+
f"{source:<{W_SOURCE}}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
print(f"\n{len(entries)} entries")
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ── Argument parser ───────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
229
|
+
parser = argparse.ArgumentParser(
|
|
230
|
+
prog="python -m sdk.cli",
|
|
231
|
+
description="Niitaka policy file manager",
|
|
232
|
+
)
|
|
233
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
234
|
+
|
|
235
|
+
# Shared backend args
|
|
236
|
+
def add_backend_args(p: argparse.ArgumentParser) -> None:
|
|
237
|
+
p.add_argument("--url", default=os.getenv("NIITAKA_BACKEND_URL", "http://localhost:8000"),
|
|
238
|
+
help="Backend URL (default: $NIITAKA_BACKEND_URL or http://localhost:8000)")
|
|
239
|
+
p.add_argument("--key", default=os.getenv("NIITAKA_API_KEY", ""),
|
|
240
|
+
help="API key (default: $NIITAKA_API_KEY)")
|
|
241
|
+
|
|
242
|
+
# validate
|
|
243
|
+
p_val = sub.add_parser("validate", help="Validate a policy file without hitting the backend")
|
|
244
|
+
p_val.add_argument("file", help="Path to policy YAML or JSON file")
|
|
245
|
+
|
|
246
|
+
# push
|
|
247
|
+
p_push = sub.add_parser("push", help="Upload policy file to backend DB")
|
|
248
|
+
p_push.add_argument("file", help="Path to policy YAML or JSON file")
|
|
249
|
+
p_push.add_argument("--dry-run", action="store_true",
|
|
250
|
+
help="Validate and show what would be pushed, but make no changes")
|
|
251
|
+
add_backend_args(p_push)
|
|
252
|
+
|
|
253
|
+
# pull
|
|
254
|
+
p_pull = sub.add_parser("pull", help="Download policies from backend as YAML")
|
|
255
|
+
p_pull.add_argument("--agent", default=None, help="Filter to a single agent_id")
|
|
256
|
+
p_pull.add_argument("-o", "--output", default=None, help="Write to file instead of stdout")
|
|
257
|
+
add_backend_args(p_pull)
|
|
258
|
+
|
|
259
|
+
# history
|
|
260
|
+
p_hist = sub.add_parser("history", help="Show the policy audit log")
|
|
261
|
+
p_hist.add_argument("--agent", default=None, help="Filter to a single agent_id")
|
|
262
|
+
p_hist.add_argument("--limit", type=int, default=50,
|
|
263
|
+
help="Max entries to show (default: 50)")
|
|
264
|
+
add_backend_args(p_hist)
|
|
265
|
+
|
|
266
|
+
return parser
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def main() -> None:
|
|
270
|
+
parser = build_parser()
|
|
271
|
+
args = parser.parse_args()
|
|
272
|
+
|
|
273
|
+
dispatch = {"validate": cmd_validate, "push": cmd_push, "pull": cmd_pull, "history": cmd_history}
|
|
274
|
+
sys.exit(dispatch[args.command](args))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
main()
|