risk-mirror 1.0.2__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.
- risk-mirror-1.0.2/PKG-INFO +78 -0
- risk-mirror-1.0.2/README.md +50 -0
- risk-mirror-1.0.2/risk_mirror/__init__.py +31 -0
- risk-mirror-1.0.2/risk_mirror/cli.py +309 -0
- risk-mirror-1.0.2/risk_mirror/client.py +394 -0
- risk-mirror-1.0.2/risk_mirror/types.py +160 -0
- risk-mirror-1.0.2/risk_mirror.egg-info/PKG-INFO +78 -0
- risk-mirror-1.0.2/risk_mirror.egg-info/SOURCES.txt +12 -0
- risk-mirror-1.0.2/risk_mirror.egg-info/dependency_links.txt +1 -0
- risk-mirror-1.0.2/risk_mirror.egg-info/entry_points.txt +3 -0
- risk-mirror-1.0.2/risk_mirror.egg-info/requires.txt +5 -0
- risk-mirror-1.0.2/risk_mirror.egg-info/top_level.txt +1 -0
- risk-mirror-1.0.2/setup.cfg +4 -0
- risk-mirror-1.0.2/setup.py +44 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: risk-mirror
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Deterministic AI Safety Toolkit - Python SDK & CLI
|
|
5
|
+
Home-page: https://github.com/myProjectsRavi/risk-mirror-core
|
|
6
|
+
Author: RTN Labs
|
|
7
|
+
Author-email: support@risk-mirror.com
|
|
8
|
+
License: UNKNOWN
|
|
9
|
+
Keywords: ai-safety,pii,secrets,prompt-security,deterministic,cli
|
|
10
|
+
Platform: UNKNOWN
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
|
|
27
|
+
# Risk Mirror SDK - Python
|
|
28
|
+
|
|
29
|
+
Deterministic AI Safety Toolkit for prompt security.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install risk-mirror
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from risk_mirror import RiskMirror
|
|
41
|
+
|
|
42
|
+
# Initialize client
|
|
43
|
+
client = RiskMirror(api_key="your-api-key")
|
|
44
|
+
|
|
45
|
+
# Scan for safety issues
|
|
46
|
+
result = client.scan("Check this text for PII and secrets")
|
|
47
|
+
|
|
48
|
+
print(result.verdict) # SAFE, REVIEW, or HIGH_RISK
|
|
49
|
+
print(result.findings) # List of findings
|
|
50
|
+
print(result.safe_output) # Redacted text
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- **PII Detection**: Email, phone, SSN, etc.
|
|
56
|
+
- **Secrets Detection**: API keys, tokens, passwords
|
|
57
|
+
- **Injection Detection**: Prompt injection attempts
|
|
58
|
+
- **Safe Share**: Burner-safe strings for sharing with AI
|
|
59
|
+
- **Zero Storage**: No content is stored
|
|
60
|
+
- **Deterministic**: Same input = same output
|
|
61
|
+
|
|
62
|
+
## Safe Share
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from risk_mirror import RiskMirror
|
|
66
|
+
|
|
67
|
+
client = RiskMirror(api_key="your-api-key")
|
|
68
|
+
result = client.safe_share("sk-ABCD1234-XYZ", mode="full")
|
|
69
|
+
|
|
70
|
+
print(result.safe_share_text)
|
|
71
|
+
print(result.audit_summary)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
77
|
+
|
|
78
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Risk Mirror SDK - Python
|
|
2
|
+
|
|
3
|
+
Deterministic AI Safety Toolkit for prompt security.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install risk-mirror
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from risk_mirror import RiskMirror
|
|
15
|
+
|
|
16
|
+
# Initialize client
|
|
17
|
+
client = RiskMirror(api_key="your-api-key")
|
|
18
|
+
|
|
19
|
+
# Scan for safety issues
|
|
20
|
+
result = client.scan("Check this text for PII and secrets")
|
|
21
|
+
|
|
22
|
+
print(result.verdict) # SAFE, REVIEW, or HIGH_RISK
|
|
23
|
+
print(result.findings) # List of findings
|
|
24
|
+
print(result.safe_output) # Redacted text
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **PII Detection**: Email, phone, SSN, etc.
|
|
30
|
+
- **Secrets Detection**: API keys, tokens, passwords
|
|
31
|
+
- **Injection Detection**: Prompt injection attempts
|
|
32
|
+
- **Safe Share**: Burner-safe strings for sharing with AI
|
|
33
|
+
- **Zero Storage**: No content is stored
|
|
34
|
+
- **Deterministic**: Same input = same output
|
|
35
|
+
|
|
36
|
+
## Safe Share
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from risk_mirror import RiskMirror
|
|
40
|
+
|
|
41
|
+
client = RiskMirror(api_key="your-api-key")
|
|
42
|
+
result = client.safe_share("sk-ABCD1234-XYZ", mode="full")
|
|
43
|
+
|
|
44
|
+
print(result.safe_share_text)
|
|
45
|
+
print(result.audit_summary)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Risk Mirror SDK - Python
|
|
3
|
+
========================
|
|
4
|
+
Deterministic, stateless AI safety toolkit
|
|
5
|
+
|
|
6
|
+
US-0001 to US-0020: SDKs (JS/Python) feature
|
|
7
|
+
"""
|
|
8
|
+
from .client import RiskMirror, RiskMirrorError, safe_share
|
|
9
|
+
from .types import (
|
|
10
|
+
ScanResponse,
|
|
11
|
+
ScanPolicy,
|
|
12
|
+
Finding,
|
|
13
|
+
AuditSummary,
|
|
14
|
+
SafeShareResponse,
|
|
15
|
+
Verdict,
|
|
16
|
+
Severity,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "1.0.2"
|
|
20
|
+
__all__ = [
|
|
21
|
+
"RiskMirror",
|
|
22
|
+
"RiskMirrorError",
|
|
23
|
+
"ScanResponse",
|
|
24
|
+
"ScanPolicy",
|
|
25
|
+
"Finding",
|
|
26
|
+
"AuditSummary",
|
|
27
|
+
"SafeShareResponse",
|
|
28
|
+
"Verdict",
|
|
29
|
+
"Severity",
|
|
30
|
+
"safe_share",
|
|
31
|
+
]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Risk Mirror CLI - API-Based Safety Scanner
|
|
4
|
+
===========================================
|
|
5
|
+
Scan prompts and files for PII, secrets, and injection risks.
|
|
6
|
+
All scans go through the Risk Mirror API for unified usage tracking.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
risk-mirror scan "Your prompt text here"
|
|
10
|
+
risk-mirror scan -f prompt.txt
|
|
11
|
+
risk-mirror scan -f prompt.txt --json
|
|
12
|
+
risk-mirror safe-share "sensitive string"
|
|
13
|
+
risk-mirror safe-share -f secrets.txt --mode full
|
|
14
|
+
risk-mirror --version
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from typing import Optional, List
|
|
22
|
+
|
|
23
|
+
from .client import RiskMirror, RiskMirrorError
|
|
24
|
+
|
|
25
|
+
CLI_VERSION = "1.0.2"
|
|
26
|
+
|
|
27
|
+
# Exit codes
|
|
28
|
+
EXIT_SAFE = 0
|
|
29
|
+
EXIT_RISK = 1
|
|
30
|
+
EXIT_ERROR = 2
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
34
|
+
"""Create argument parser."""
|
|
35
|
+
parser = argparse.ArgumentParser(
|
|
36
|
+
prog="risk-mirror",
|
|
37
|
+
description="Risk Mirror CLI - Deterministic AI Safety Scanner",
|
|
38
|
+
epilog="Examples:\n"
|
|
39
|
+
" risk-mirror scan \"Check this prompt\"\n"
|
|
40
|
+
" risk-mirror scan -f prompt.txt --json\n"
|
|
41
|
+
" RISK_MIRROR_API_KEY=rm_xxx risk-mirror scan \"text\"\n",
|
|
42
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--version", "-V",
|
|
47
|
+
action="version",
|
|
48
|
+
version=f"risk-mirror CLI {CLI_VERSION}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
52
|
+
|
|
53
|
+
# Scan command
|
|
54
|
+
scan_parser = subparsers.add_parser(
|
|
55
|
+
"scan",
|
|
56
|
+
help="Scan text or file for safety issues",
|
|
57
|
+
description="Scan text or file for PII, secrets, and prompt injection"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
scan_parser.add_argument(
|
|
61
|
+
"text",
|
|
62
|
+
nargs="?",
|
|
63
|
+
help="Text to scan (use -f for file input)"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
scan_parser.add_argument(
|
|
67
|
+
"-f", "--file",
|
|
68
|
+
help="File to scan"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
scan_parser.add_argument(
|
|
72
|
+
"--api-key", "-k",
|
|
73
|
+
help="API key (or set RISK_MIRROR_API_KEY env var)"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
scan_parser.add_argument(
|
|
77
|
+
"--json", "-j",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Output as JSON"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
scan_parser.add_argument(
|
|
83
|
+
"--quiet", "-q",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="Only output verdict (for CI/CD)"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Safe Share command
|
|
89
|
+
safe_parser = subparsers.add_parser(
|
|
90
|
+
"safe-share",
|
|
91
|
+
help="Generate Safe Share burner text",
|
|
92
|
+
description="Create non-reversible, format-preserving safe share text"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
safe_parser.add_argument(
|
|
96
|
+
"text",
|
|
97
|
+
nargs="?",
|
|
98
|
+
help="Text to transform (use -f for file input)"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
safe_parser.add_argument(
|
|
102
|
+
"-f", "--file",
|
|
103
|
+
help="File to transform"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
safe_parser.add_argument(
|
|
107
|
+
"--mode",
|
|
108
|
+
choices=["full", "selective"],
|
|
109
|
+
default="selective",
|
|
110
|
+
help="Replacement mode (default: selective)"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
safe_parser.add_argument(
|
|
114
|
+
"--no-secrets",
|
|
115
|
+
action="store_true",
|
|
116
|
+
help="Disable secrets replacement in selective mode"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
safe_parser.add_argument(
|
|
120
|
+
"--allow-valid",
|
|
121
|
+
action="store_true",
|
|
122
|
+
help="Allow valid-looking values (disables strict invalid generation)"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
safe_parser.add_argument(
|
|
126
|
+
"--api-key", "-k",
|
|
127
|
+
help="API key (or set RISK_MIRROR_API_KEY env var)"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
safe_parser.add_argument(
|
|
131
|
+
"--json", "-j",
|
|
132
|
+
action="store_true",
|
|
133
|
+
help="Output as JSON"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
safe_parser.add_argument(
|
|
137
|
+
"--quiet", "-q",
|
|
138
|
+
action="store_true",
|
|
139
|
+
help="Only output the safe share text"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return parser
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def format_result(result, as_json: bool = False) -> str:
|
|
146
|
+
"""Format scan result for display."""
|
|
147
|
+
if as_json:
|
|
148
|
+
return json.dumps({
|
|
149
|
+
"verdict": result.verdict.value if hasattr(result.verdict, 'value') else str(result.verdict),
|
|
150
|
+
"safe_output": result.safe_output,
|
|
151
|
+
"findings_count": len(result.findings) if result.findings else 0,
|
|
152
|
+
}, indent=2)
|
|
153
|
+
|
|
154
|
+
verdict = result.verdict.value if hasattr(result.verdict, 'value') else str(result.verdict)
|
|
155
|
+
emoji = {"SAFE": "✅", "REVIEW": "⚠️", "BLOCK": "🚨"}.get(verdict, "❓")
|
|
156
|
+
|
|
157
|
+
lines = [
|
|
158
|
+
f"\n{emoji} Verdict: {verdict}",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
if result.findings:
|
|
162
|
+
lines.append(f"\n📋 Findings ({len(result.findings)}):")
|
|
163
|
+
for f in result.findings:
|
|
164
|
+
lines.append(f" • {f.get('category', 'unknown')}: {f.get('count', 1)} occurrence(s)")
|
|
165
|
+
|
|
166
|
+
if result.safe_output and result.safe_output != result.input_text:
|
|
167
|
+
lines.append(f"\n🔒 Safe Output:\n{result.safe_output[:500]}{'...' if len(result.safe_output) > 500 else ''}")
|
|
168
|
+
|
|
169
|
+
lines.append("\n🔐 Privacy: Stateless scan, no content stored")
|
|
170
|
+
|
|
171
|
+
return "\n".join(lines)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main(args: Optional[List[str]] = None) -> int:
|
|
175
|
+
"""Main CLI entry point."""
|
|
176
|
+
parser = create_parser()
|
|
177
|
+
parsed = parser.parse_args(args)
|
|
178
|
+
|
|
179
|
+
if not parsed.command:
|
|
180
|
+
parser.print_help()
|
|
181
|
+
return EXIT_ERROR
|
|
182
|
+
|
|
183
|
+
if parsed.command == "scan":
|
|
184
|
+
return handle_scan(parsed)
|
|
185
|
+
if parsed.command == "safe-share":
|
|
186
|
+
return handle_safe_share(parsed)
|
|
187
|
+
|
|
188
|
+
return EXIT_ERROR
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def handle_scan(args: argparse.Namespace) -> int:
|
|
192
|
+
"""Handle scan command."""
|
|
193
|
+
# Get API key
|
|
194
|
+
api_key = args.api_key or os.environ.get("RISK_MIRROR_API_KEY")
|
|
195
|
+
|
|
196
|
+
# Get input text
|
|
197
|
+
if args.file:
|
|
198
|
+
try:
|
|
199
|
+
with open(args.file, "r", encoding="utf-8") as f:
|
|
200
|
+
text = f.read()
|
|
201
|
+
except FileNotFoundError:
|
|
202
|
+
print(f"Error: File not found: {args.file}", file=sys.stderr)
|
|
203
|
+
return EXIT_ERROR
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def handle_safe_share(args: argparse.Namespace) -> int:
|
|
207
|
+
"""Handle safe-share command."""
|
|
208
|
+
api_key = args.api_key or os.environ.get("RISK_MIRROR_API_KEY")
|
|
209
|
+
|
|
210
|
+
if args.file:
|
|
211
|
+
try:
|
|
212
|
+
with open(args.file, "r", encoding="utf-8") as f:
|
|
213
|
+
text = f.read()
|
|
214
|
+
except FileNotFoundError:
|
|
215
|
+
print(f"Error: File not found: {args.file}", file=sys.stderr)
|
|
216
|
+
return EXIT_ERROR
|
|
217
|
+
except Exception as e:
|
|
218
|
+
print(f"Error reading file: {e}", file=sys.stderr)
|
|
219
|
+
return EXIT_ERROR
|
|
220
|
+
elif args.text:
|
|
221
|
+
text = args.text
|
|
222
|
+
else:
|
|
223
|
+
if sys.stdin.isatty():
|
|
224
|
+
print("Error: No input provided. Use -f FILE or provide text.", file=sys.stderr)
|
|
225
|
+
return EXIT_ERROR
|
|
226
|
+
text = sys.stdin.read()
|
|
227
|
+
|
|
228
|
+
if not text.strip():
|
|
229
|
+
print("Error: Empty input", file=sys.stderr)
|
|
230
|
+
return EXIT_ERROR
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
client = RiskMirror(api_key=api_key)
|
|
234
|
+
result = client.safe_share(
|
|
235
|
+
text,
|
|
236
|
+
mode=args.mode,
|
|
237
|
+
include_secrets=not args.no_secrets,
|
|
238
|
+
strict_invalid=not args.allow_valid,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if args.quiet:
|
|
242
|
+
print(result.safe_share_text)
|
|
243
|
+
return EXIT_SAFE
|
|
244
|
+
|
|
245
|
+
if args.json:
|
|
246
|
+
print(json.dumps({
|
|
247
|
+
"safe_share_text": result.safe_share_text,
|
|
248
|
+
"mode": result.mode,
|
|
249
|
+
"audit_summary": result.audit_summary,
|
|
250
|
+
}, indent=2))
|
|
251
|
+
else:
|
|
252
|
+
print(result.safe_share_text)
|
|
253
|
+
if result.audit_summary:
|
|
254
|
+
print(f"\n🔐 Replaced spans: {result.audit_summary.get('spans_replaced', 0)}")
|
|
255
|
+
print(f"🧩 Replaced chars: {result.audit_summary.get('chars_replaced', 0)}")
|
|
256
|
+
print("\n🔐 Privacy: Stateless, no content stored")
|
|
257
|
+
|
|
258
|
+
return EXIT_SAFE
|
|
259
|
+
except RiskMirrorError as e:
|
|
260
|
+
print(f"API Error: {e.message}", file=sys.stderr)
|
|
261
|
+
if e.status_code == 401:
|
|
262
|
+
print("Hint: Set RISK_MIRROR_API_KEY or use --api-key", file=sys.stderr)
|
|
263
|
+
return EXIT_ERROR
|
|
264
|
+
except Exception as e:
|
|
265
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
266
|
+
return EXIT_ERROR
|
|
267
|
+
except Exception as e:
|
|
268
|
+
print(f"Error reading file: {e}", file=sys.stderr)
|
|
269
|
+
return EXIT_ERROR
|
|
270
|
+
elif args.text:
|
|
271
|
+
text = args.text
|
|
272
|
+
else:
|
|
273
|
+
# Read from stdin
|
|
274
|
+
if sys.stdin.isatty():
|
|
275
|
+
print("Error: No input provided. Use -f FILE or provide text.", file=sys.stderr)
|
|
276
|
+
return EXIT_ERROR
|
|
277
|
+
text = sys.stdin.read()
|
|
278
|
+
|
|
279
|
+
if not text.strip():
|
|
280
|
+
print("Error: Empty input", file=sys.stderr)
|
|
281
|
+
return EXIT_ERROR
|
|
282
|
+
|
|
283
|
+
# Create client and scan
|
|
284
|
+
try:
|
|
285
|
+
client = RiskMirror(api_key=api_key)
|
|
286
|
+
result = client.scan(text)
|
|
287
|
+
|
|
288
|
+
if args.quiet:
|
|
289
|
+
verdict = result.verdict.value if hasattr(result.verdict, 'value') else str(result.verdict)
|
|
290
|
+
print(verdict)
|
|
291
|
+
else:
|
|
292
|
+
print(format_result(result, as_json=args.json))
|
|
293
|
+
|
|
294
|
+
# Return exit code based on verdict
|
|
295
|
+
verdict = result.verdict.value if hasattr(result.verdict, 'value') else str(result.verdict)
|
|
296
|
+
return EXIT_SAFE if verdict == "SAFE" else EXIT_RISK
|
|
297
|
+
|
|
298
|
+
except RiskMirrorError as e:
|
|
299
|
+
print(f"API Error: {e.message}", file=sys.stderr)
|
|
300
|
+
if e.status_code == 401:
|
|
301
|
+
print("Hint: Set RISK_MIRROR_API_KEY or use --api-key", file=sys.stderr)
|
|
302
|
+
return EXIT_ERROR
|
|
303
|
+
except Exception as e:
|
|
304
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
305
|
+
return EXIT_ERROR
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
if __name__ == "__main__":
|
|
309
|
+
sys.exit(main())
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Risk Mirror SDK - Python Client
|
|
3
|
+
================================
|
|
4
|
+
Deterministic, stateless AI safety toolkit
|
|
5
|
+
Drop-in integration for prompt security
|
|
6
|
+
|
|
7
|
+
US-0001: Core Behavior
|
|
8
|
+
US-0002: Policy Controls
|
|
9
|
+
US-0006: Edge Cases
|
|
10
|
+
US-0007: Performance
|
|
11
|
+
US-0008: Security
|
|
12
|
+
US-0009: Audit Evidence
|
|
13
|
+
US-0010: Privacy (no storage)
|
|
14
|
+
US-0011: Compliance
|
|
15
|
+
US-0012: Rate Limits
|
|
16
|
+
US-0013: Logging
|
|
17
|
+
US-0015: Rollback
|
|
18
|
+
US-0020: Error Handling
|
|
19
|
+
"""
|
|
20
|
+
import time
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Optional, Callable, Dict, Any
|
|
23
|
+
from urllib.request import Request, urlopen
|
|
24
|
+
from urllib.error import HTTPError, URLError
|
|
25
|
+
import json
|
|
26
|
+
|
|
27
|
+
from .types import ScanResponse, ScanPolicy, OptimizeResponse, SafeShareResponse, Verdict
|
|
28
|
+
|
|
29
|
+
# Constants
|
|
30
|
+
DEFAULT_BASE_URL = "https://risk-mirror-auth.anonymous617461746174.workers.dev"
|
|
31
|
+
DEFAULT_TIMEOUT = 30
|
|
32
|
+
DEFAULT_RETRIES = 3
|
|
33
|
+
SDK_VERSION = "1.0.2"
|
|
34
|
+
ENGINE_VERSION = "6.0-ULTRA"
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("risk_mirror")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RiskMirrorError(Exception):
|
|
40
|
+
"""Base exception for Risk Mirror SDK.
|
|
41
|
+
|
|
42
|
+
US-0020: Error Handling
|
|
43
|
+
"""
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
code: str,
|
|
47
|
+
message: str,
|
|
48
|
+
retryable: bool = False,
|
|
49
|
+
status_code: Optional[int] = None
|
|
50
|
+
):
|
|
51
|
+
super().__init__(message)
|
|
52
|
+
self.code = code
|
|
53
|
+
self.message = message
|
|
54
|
+
self.retryable = retryable
|
|
55
|
+
self.status_code = status_code
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RiskMirror:
|
|
59
|
+
"""
|
|
60
|
+
Risk Mirror SDK Client
|
|
61
|
+
======================
|
|
62
|
+
Deterministic AI safety scanning with zero content storage.
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> client = RiskMirror(api_key="your-key")
|
|
66
|
+
>>> result = client.scan("Check this prompt for safety")
|
|
67
|
+
>>> print(result.verdict)
|
|
68
|
+
'SAFE'
|
|
69
|
+
|
|
70
|
+
US-0001: Core Behavior
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
76
|
+
api_key: Optional[str] = None,
|
|
77
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
78
|
+
retries: int = DEFAULT_RETRIES,
|
|
79
|
+
debug: bool = False,
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Initialize the Risk Mirror client.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
base_url: API base URL
|
|
86
|
+
api_key: Optional API key for authentication
|
|
87
|
+
timeout: Request timeout in seconds
|
|
88
|
+
retries: Number of retries on failure
|
|
89
|
+
debug: Enable debug logging
|
|
90
|
+
"""
|
|
91
|
+
self.base_url = base_url.rstrip("/")
|
|
92
|
+
self.api_key = api_key
|
|
93
|
+
self.timeout = timeout
|
|
94
|
+
self.retries = retries
|
|
95
|
+
self.debug = debug
|
|
96
|
+
self._telemetry_callback: Optional[Callable[[Dict[str, Any]], None]] = None
|
|
97
|
+
|
|
98
|
+
if debug:
|
|
99
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
100
|
+
|
|
101
|
+
def on_telemetry(self, callback: Callable[[Dict[str, Any]], None]) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Set telemetry callback (receives no content, only metadata).
|
|
104
|
+
|
|
105
|
+
US-0018: Analytics without storing content
|
|
106
|
+
"""
|
|
107
|
+
self._telemetry_callback = callback
|
|
108
|
+
|
|
109
|
+
def _log(self, msg: str, *args: Any) -> None:
|
|
110
|
+
if self.debug:
|
|
111
|
+
logger.debug(f"[RiskMirror] {msg}", *args)
|
|
112
|
+
|
|
113
|
+
def _request(
|
|
114
|
+
self,
|
|
115
|
+
endpoint: str,
|
|
116
|
+
method: str = "POST",
|
|
117
|
+
body: Optional[Dict[str, Any]] = None
|
|
118
|
+
) -> Dict[str, Any]:
|
|
119
|
+
"""Make HTTP request with retry logic.
|
|
120
|
+
|
|
121
|
+
US-0012: Rate Limits
|
|
122
|
+
US-0020: Error Handling
|
|
123
|
+
"""
|
|
124
|
+
url = f"{self.base_url}{endpoint}"
|
|
125
|
+
headers = {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
"X-SDK-Version": SDK_VERSION,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if self.api_key:
|
|
131
|
+
headers["X-API-Key"] = self.api_key
|
|
132
|
+
|
|
133
|
+
last_error: Optional[Exception] = None
|
|
134
|
+
|
|
135
|
+
for attempt in range(self.retries + 1):
|
|
136
|
+
try:
|
|
137
|
+
self._log(f"Request attempt {attempt + 1}: {method} {endpoint}")
|
|
138
|
+
|
|
139
|
+
data = json.dumps(body).encode("utf-8") if body else None
|
|
140
|
+
req = Request(url, data=data, headers=headers, method=method)
|
|
141
|
+
|
|
142
|
+
with urlopen(req, timeout=self.timeout) as response:
|
|
143
|
+
return json.loads(response.read().decode("utf-8"))
|
|
144
|
+
|
|
145
|
+
except HTTPError as e:
|
|
146
|
+
last_error = e
|
|
147
|
+
|
|
148
|
+
# US-0012: Rate limit handling
|
|
149
|
+
if e.code == 429:
|
|
150
|
+
retry_after = e.headers.get("Retry-After", "1")
|
|
151
|
+
wait_secs = int(retry_after)
|
|
152
|
+
self._log(f"Rate limited, waiting {wait_secs}s")
|
|
153
|
+
time.sleep(wait_secs)
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
# Retryable server errors
|
|
157
|
+
if e.code >= 500 and attempt < self.retries:
|
|
158
|
+
wait_secs = min(2 ** attempt, 10)
|
|
159
|
+
self._log(f"Server error {e.code}, retry in {wait_secs}s")
|
|
160
|
+
time.sleep(wait_secs)
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
raise RiskMirrorError(
|
|
164
|
+
code="API_ERROR",
|
|
165
|
+
message=f"API returned {e.code}: {e.read().decode('utf-8', errors='ignore')}",
|
|
166
|
+
retryable=e.code >= 500,
|
|
167
|
+
status_code=e.code
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
except URLError as e:
|
|
171
|
+
last_error = e
|
|
172
|
+
if attempt < self.retries:
|
|
173
|
+
wait_secs = min(2 ** attempt, 10)
|
|
174
|
+
self._log(f"Network error, retry in {wait_secs}s: {e.reason}")
|
|
175
|
+
time.sleep(wait_secs)
|
|
176
|
+
continue
|
|
177
|
+
raise RiskMirrorError(
|
|
178
|
+
code="NETWORK_ERROR",
|
|
179
|
+
message=str(e.reason),
|
|
180
|
+
retryable=True
|
|
181
|
+
)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
last_error = e
|
|
184
|
+
raise RiskMirrorError(
|
|
185
|
+
code="UNKNOWN_ERROR",
|
|
186
|
+
message=str(e),
|
|
187
|
+
retryable=False
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
raise last_error or RiskMirrorError("UNKNOWN_ERROR", "Request failed", False)
|
|
191
|
+
|
|
192
|
+
def scan(
|
|
193
|
+
self,
|
|
194
|
+
input_text: str,
|
|
195
|
+
policy: Optional[ScanPolicy] = None,
|
|
196
|
+
mode: str = "default"
|
|
197
|
+
) -> ScanResponse:
|
|
198
|
+
"""
|
|
199
|
+
Scan input for safety issues.
|
|
200
|
+
|
|
201
|
+
US-0001: Core behavior - deterministic scanning
|
|
202
|
+
US-0002: Policy controls - configurable detection
|
|
203
|
+
US-0010: Privacy - no content storage
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
input_text: Text to scan
|
|
207
|
+
policy: Optional policy configuration
|
|
208
|
+
mode: Scan mode (default, strict, paranoid)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
ScanResponse with verdict, findings, and safe_output
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
RiskMirrorError: On validation or API errors
|
|
215
|
+
"""
|
|
216
|
+
start = time.perf_counter()
|
|
217
|
+
|
|
218
|
+
# US-0006: Edge cases - input validation
|
|
219
|
+
if not input_text or not isinstance(input_text, str):
|
|
220
|
+
raise RiskMirrorError(
|
|
221
|
+
code="INVALID_INPUT",
|
|
222
|
+
message="Input must be a non-empty string",
|
|
223
|
+
retryable=False
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# US-0008: Security - size limit
|
|
227
|
+
max_length = policy.max_length if policy else 100000
|
|
228
|
+
if len(input_text) > max_length:
|
|
229
|
+
raise RiskMirrorError(
|
|
230
|
+
code="INPUT_TOO_LARGE",
|
|
231
|
+
message=f"Input exceeds maximum length of {max_length} characters",
|
|
232
|
+
retryable=False
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Build request
|
|
236
|
+
policy = policy or ScanPolicy()
|
|
237
|
+
request_body = {
|
|
238
|
+
"prompt": input_text,
|
|
239
|
+
"policy": {
|
|
240
|
+
"detect_pii": policy.pii,
|
|
241
|
+
"detect_secrets": policy.secrets,
|
|
242
|
+
"detect_injection": policy.injection,
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if policy.pii_classes:
|
|
247
|
+
request_body["policy"]["pii_classes"] = policy.pii_classes
|
|
248
|
+
|
|
249
|
+
response_data = self._request("/firewall/audit", "POST", request_body)
|
|
250
|
+
|
|
251
|
+
latency_ms = (time.perf_counter() - start) * 1000
|
|
252
|
+
response_data["latency_ms"] = latency_ms
|
|
253
|
+
response_data["privacy"] = {"stateless": True, "content_logged": False}
|
|
254
|
+
|
|
255
|
+
# US-0018: Analytics telemetry (no content)
|
|
256
|
+
if self._telemetry_callback:
|
|
257
|
+
self._telemetry_callback({
|
|
258
|
+
"operation": "scan",
|
|
259
|
+
"verdict": response_data.get("verdict", "SAFE"),
|
|
260
|
+
"findings_count": len(response_data.get("findings", [])),
|
|
261
|
+
"latency_ms": latency_ms,
|
|
262
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
return ScanResponse.from_dict(response_data)
|
|
266
|
+
|
|
267
|
+
def audit(
|
|
268
|
+
self,
|
|
269
|
+
input_text: str,
|
|
270
|
+
policy: Optional[ScanPolicy] = None
|
|
271
|
+
) -> ScanResponse:
|
|
272
|
+
"""
|
|
273
|
+
Audit scan for compliance reporting.
|
|
274
|
+
|
|
275
|
+
US-0009: Audit Evidence
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
input_text: Text to audit
|
|
279
|
+
policy: Optional policy configuration
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
ScanResponse with detailed audit evidence
|
|
283
|
+
"""
|
|
284
|
+
return self.scan(input_text, policy, mode="strict")
|
|
285
|
+
|
|
286
|
+
def optimize(
|
|
287
|
+
self,
|
|
288
|
+
input_text: str,
|
|
289
|
+
mode: str = "compress"
|
|
290
|
+
) -> OptimizeResponse:
|
|
291
|
+
"""
|
|
292
|
+
Optimize prompt for token efficiency.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
input_text: Text to optimize
|
|
296
|
+
mode: compress, refine, or strip
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
OptimizeResponse with optimized text
|
|
300
|
+
"""
|
|
301
|
+
if not input_text or not isinstance(input_text, str):
|
|
302
|
+
raise RiskMirrorError(
|
|
303
|
+
code="INVALID_INPUT",
|
|
304
|
+
message="Input must be a non-empty string",
|
|
305
|
+
retryable=False
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
response_data = self._request("/optimize/prompt", "POST", {
|
|
309
|
+
"prompt": input_text,
|
|
310
|
+
"mode": mode,
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
return OptimizeResponse.from_dict(response_data)
|
|
314
|
+
|
|
315
|
+
def safe_share(
|
|
316
|
+
self,
|
|
317
|
+
input_text: str,
|
|
318
|
+
mode: str = "selective",
|
|
319
|
+
include_secrets: bool = True,
|
|
320
|
+
strict_invalid: bool = True
|
|
321
|
+
) -> SafeShareResponse:
|
|
322
|
+
"""
|
|
323
|
+
Generate Safe Share burner text (non-reversible).
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
input_text: Text to transform
|
|
327
|
+
mode: full or selective
|
|
328
|
+
include_secrets: Replace secrets in selective mode
|
|
329
|
+
strict_invalid: Force invalid CC/SSN/etc
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
SafeShareResponse with safe_share_text and audit summary
|
|
333
|
+
"""
|
|
334
|
+
if not input_text or not isinstance(input_text, str):
|
|
335
|
+
raise RiskMirrorError(
|
|
336
|
+
code="INVALID_INPUT",
|
|
337
|
+
message="Input must be a non-empty string",
|
|
338
|
+
retryable=False
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
response_data = self._request("/safe-share/text", "POST", {
|
|
342
|
+
"text": input_text,
|
|
343
|
+
"mode": mode,
|
|
344
|
+
"include_secrets": include_secrets,
|
|
345
|
+
"strict_invalid": strict_invalid,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
return SafeShareResponse.from_dict(response_data)
|
|
349
|
+
|
|
350
|
+
def get_version(self) -> Dict[str, str]:
|
|
351
|
+
"""
|
|
352
|
+
Get SDK and engine version.
|
|
353
|
+
|
|
354
|
+
US-0015: Rollback - version tracking
|
|
355
|
+
"""
|
|
356
|
+
return {
|
|
357
|
+
"sdk": SDK_VERSION,
|
|
358
|
+
"engine": ENGINE_VERSION,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ============ Quick Helpers ============
|
|
363
|
+
_default_client: Optional[RiskMirror] = None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def configure(**kwargs: Any) -> None:
|
|
367
|
+
"""Configure the default client."""
|
|
368
|
+
global _default_client
|
|
369
|
+
_default_client = RiskMirror(**kwargs)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def scan(
|
|
373
|
+
input_text: str,
|
|
374
|
+
policy: Optional[ScanPolicy] = None,
|
|
375
|
+
mode: str = "default"
|
|
376
|
+
) -> ScanResponse:
|
|
377
|
+
"""Quick scan using default client."""
|
|
378
|
+
global _default_client
|
|
379
|
+
if _default_client is None:
|
|
380
|
+
_default_client = RiskMirror()
|
|
381
|
+
return _default_client.scan(input_text, policy, mode)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def safe_share(
|
|
385
|
+
input_text: str,
|
|
386
|
+
mode: str = "selective",
|
|
387
|
+
include_secrets: bool = True,
|
|
388
|
+
strict_invalid: bool = True
|
|
389
|
+
) -> SafeShareResponse:
|
|
390
|
+
"""Quick safe share using default client."""
|
|
391
|
+
global _default_client
|
|
392
|
+
if _default_client is None:
|
|
393
|
+
_default_client = RiskMirror()
|
|
394
|
+
return _default_client.safe_share(input_text, mode, include_secrets, strict_invalid)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Risk Mirror SDK - Type Definitions
|
|
3
|
+
==================================
|
|
4
|
+
Dataclasses for SDK request/response models
|
|
5
|
+
|
|
6
|
+
US-0004: API Surface
|
|
7
|
+
US-0005: Data Model
|
|
8
|
+
"""
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import List, Optional, Dict, Any, Literal
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Verdict(str, Enum):
|
|
15
|
+
SAFE = "SAFE"
|
|
16
|
+
REVIEW = "REVIEW"
|
|
17
|
+
HIGH_RISK = "HIGH_RISK"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Severity(str, Enum):
|
|
21
|
+
LOW = "LOW"
|
|
22
|
+
MED = "MED"
|
|
23
|
+
HIGH = "HIGH"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Finding:
|
|
28
|
+
"""A single finding from the scan."""
|
|
29
|
+
category: str
|
|
30
|
+
severity: Severity
|
|
31
|
+
count: int
|
|
32
|
+
match: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AuditSummary:
|
|
37
|
+
"""Summary of modifications made."""
|
|
38
|
+
phrases_removed: int = 0
|
|
39
|
+
pii_redacted: int = 0
|
|
40
|
+
secrets_masked: int = 0
|
|
41
|
+
injections_blocked: int = 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ScanPolicy:
|
|
46
|
+
"""Configuration for scan behavior.
|
|
47
|
+
|
|
48
|
+
US-0002: Policy Controls
|
|
49
|
+
"""
|
|
50
|
+
pii: bool = True
|
|
51
|
+
secrets: bool = True
|
|
52
|
+
injection: bool = True
|
|
53
|
+
pii_classes: Optional[List[str]] = None
|
|
54
|
+
max_length: int = 100000
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Privacy:
|
|
59
|
+
"""Privacy guarantees."""
|
|
60
|
+
stateless: bool = True
|
|
61
|
+
content_logged: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ScanResponse:
|
|
66
|
+
"""Response from scan operation.
|
|
67
|
+
|
|
68
|
+
US-0009: Audit Evidence
|
|
69
|
+
US-0010: Privacy
|
|
70
|
+
US-0011: Compliance
|
|
71
|
+
"""
|
|
72
|
+
verdict: Verdict
|
|
73
|
+
findings: List[Finding]
|
|
74
|
+
safe_output: str
|
|
75
|
+
audit_summary: AuditSummary
|
|
76
|
+
policy_hash: str
|
|
77
|
+
engine_version: str
|
|
78
|
+
latency_ms: float
|
|
79
|
+
privacy: Optional[Privacy] = None
|
|
80
|
+
compliance_tags: List[str] = field(default_factory=list)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ScanResponse":
|
|
84
|
+
"""Create ScanResponse from API response dict."""
|
|
85
|
+
findings = [
|
|
86
|
+
Finding(
|
|
87
|
+
category=f.get("category", "unknown"),
|
|
88
|
+
severity=Severity(f.get("severity", "LOW")),
|
|
89
|
+
count=f.get("count", 1),
|
|
90
|
+
match=f.get("match"),
|
|
91
|
+
)
|
|
92
|
+
for f in data.get("findings", [])
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
audit_data = data.get("audit_summary", {})
|
|
96
|
+
audit_summary = AuditSummary(
|
|
97
|
+
phrases_removed=audit_data.get("phrases_removed", 0),
|
|
98
|
+
pii_redacted=audit_data.get("pii_redacted", 0),
|
|
99
|
+
secrets_masked=audit_data.get("secrets_masked", 0),
|
|
100
|
+
injections_blocked=audit_data.get("injections_blocked", 0),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
privacy_data = data.get("privacy")
|
|
104
|
+
privacy = Privacy(
|
|
105
|
+
stateless=privacy_data.get("stateless", True),
|
|
106
|
+
content_logged=privacy_data.get("content_logged", False),
|
|
107
|
+
) if privacy_data else Privacy()
|
|
108
|
+
|
|
109
|
+
return cls(
|
|
110
|
+
verdict=Verdict(data.get("verdict", "SAFE")),
|
|
111
|
+
findings=findings,
|
|
112
|
+
safe_output=data.get("safe_output", ""),
|
|
113
|
+
audit_summary=audit_summary,
|
|
114
|
+
policy_hash=data.get("policy_hash", ""),
|
|
115
|
+
engine_version=data.get("engine_version", "6.0-ULTRA"),
|
|
116
|
+
latency_ms=data.get("latency_ms", 0.0),
|
|
117
|
+
privacy=privacy,
|
|
118
|
+
compliance_tags=data.get("compliance_tags", []),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class OptimizeResponse:
|
|
124
|
+
"""Response from optimize operation."""
|
|
125
|
+
optimized: str
|
|
126
|
+
tokens_saved: int
|
|
127
|
+
compression_ratio: float
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_dict(cls, data: Dict[str, Any]) -> "OptimizeResponse":
|
|
131
|
+
return cls(
|
|
132
|
+
optimized=data.get("optimized", ""),
|
|
133
|
+
tokens_saved=data.get("tokens_saved", 0),
|
|
134
|
+
compression_ratio=data.get("compression_ratio", 1.0),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class SafeShareResponse:
|
|
140
|
+
"""Response from Safe Share operation."""
|
|
141
|
+
safe_share_text: str
|
|
142
|
+
mode: str
|
|
143
|
+
audit_summary: Dict[str, Any]
|
|
144
|
+
engine_version: str
|
|
145
|
+
privacy: Optional[Privacy] = None
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SafeShareResponse":
|
|
149
|
+
privacy_data = data.get("privacy")
|
|
150
|
+
privacy = Privacy(
|
|
151
|
+
stateless=privacy_data.get("stateless", True),
|
|
152
|
+
content_logged=False,
|
|
153
|
+
) if privacy_data else Privacy()
|
|
154
|
+
return cls(
|
|
155
|
+
safe_share_text=data.get("safe_share_text", ""),
|
|
156
|
+
mode=data.get("mode", "selective"),
|
|
157
|
+
audit_summary=data.get("audit_summary", {}),
|
|
158
|
+
engine_version=data.get("engine_version", ""),
|
|
159
|
+
privacy=privacy,
|
|
160
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: risk-mirror
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Deterministic AI Safety Toolkit - Python SDK & CLI
|
|
5
|
+
Home-page: https://github.com/myProjectsRavi/risk-mirror-core
|
|
6
|
+
Author: RTN Labs
|
|
7
|
+
Author-email: support@risk-mirror.com
|
|
8
|
+
License: UNKNOWN
|
|
9
|
+
Keywords: ai-safety,pii,secrets,prompt-security,deterministic,cli
|
|
10
|
+
Platform: UNKNOWN
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
|
|
27
|
+
# Risk Mirror SDK - Python
|
|
28
|
+
|
|
29
|
+
Deterministic AI Safety Toolkit for prompt security.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install risk-mirror
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from risk_mirror import RiskMirror
|
|
41
|
+
|
|
42
|
+
# Initialize client
|
|
43
|
+
client = RiskMirror(api_key="your-api-key")
|
|
44
|
+
|
|
45
|
+
# Scan for safety issues
|
|
46
|
+
result = client.scan("Check this text for PII and secrets")
|
|
47
|
+
|
|
48
|
+
print(result.verdict) # SAFE, REVIEW, or HIGH_RISK
|
|
49
|
+
print(result.findings) # List of findings
|
|
50
|
+
print(result.safe_output) # Redacted text
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- **PII Detection**: Email, phone, SSN, etc.
|
|
56
|
+
- **Secrets Detection**: API keys, tokens, passwords
|
|
57
|
+
- **Injection Detection**: Prompt injection attempts
|
|
58
|
+
- **Safe Share**: Burner-safe strings for sharing with AI
|
|
59
|
+
- **Zero Storage**: No content is stored
|
|
60
|
+
- **Deterministic**: Same input = same output
|
|
61
|
+
|
|
62
|
+
## Safe Share
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from risk_mirror import RiskMirror
|
|
66
|
+
|
|
67
|
+
client = RiskMirror(api_key="your-api-key")
|
|
68
|
+
result = client.safe_share("sk-ABCD1234-XYZ", mode="full")
|
|
69
|
+
|
|
70
|
+
print(result.safe_share_text)
|
|
71
|
+
print(result.audit_summary)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
77
|
+
|
|
78
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
risk_mirror/__init__.py
|
|
4
|
+
risk_mirror/cli.py
|
|
5
|
+
risk_mirror/client.py
|
|
6
|
+
risk_mirror/types.py
|
|
7
|
+
risk_mirror.egg-info/PKG-INFO
|
|
8
|
+
risk_mirror.egg-info/SOURCES.txt
|
|
9
|
+
risk_mirror.egg-info/dependency_links.txt
|
|
10
|
+
risk_mirror.egg-info/entry_points.txt
|
|
11
|
+
risk_mirror.egg-info/requires.txt
|
|
12
|
+
risk_mirror.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
risk_mirror
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Risk Mirror SDK - Python Package Setup
|
|
3
|
+
"""
|
|
4
|
+
from setuptools import setup, find_packages
|
|
5
|
+
|
|
6
|
+
with open("README.md", "r", encoding="utf-8") as f:
|
|
7
|
+
long_description = f.read() if f else ""
|
|
8
|
+
|
|
9
|
+
setup(
|
|
10
|
+
name="risk-mirror",
|
|
11
|
+
version="1.0.2",
|
|
12
|
+
author="RTN Labs",
|
|
13
|
+
author_email="support@risk-mirror.com",
|
|
14
|
+
description="Deterministic AI Safety Toolkit - Python SDK & CLI",
|
|
15
|
+
long_description=long_description,
|
|
16
|
+
long_description_content_type="text/markdown",
|
|
17
|
+
url="https://github.com/myProjectsRavi/risk-mirror-core",
|
|
18
|
+
packages=find_packages(),
|
|
19
|
+
classifiers=[
|
|
20
|
+
"Development Status :: 5 - Production/Stable",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.8",
|
|
26
|
+
"Programming Language :: Python :: 3.9",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Topic :: Security",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
+
],
|
|
33
|
+
python_requires=">=3.8",
|
|
34
|
+
install_requires=[], # Zero dependencies - uses stdlib only
|
|
35
|
+
extras_require={
|
|
36
|
+
"dev": ["pytest", "pytest-cov", "mypy"],
|
|
37
|
+
},
|
|
38
|
+
entry_points={
|
|
39
|
+
"console_scripts": [
|
|
40
|
+
"risk-mirror=risk_mirror.cli:main",
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
keywords=["ai-safety", "pii", "secrets", "prompt-security", "deterministic", "cli"],
|
|
44
|
+
)
|