promptlatch 0.2.1__py3-none-any.whl
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.
- promptcloak/__init__.py +40 -0
- promptcloak/asgi.py +3 -0
- promptcloak/audit.py +1 -0
- promptcloak/cli.py +3 -0
- promptcloak/compat.py +1 -0
- promptcloak/config.py +1 -0
- promptcloak/library.py +1 -0
- promptcloak/patterns.py +1 -0
- promptcloak/proxy.py +3 -0
- promptcloak/py.typed +1 -0
- promptcloak/redaction.py +1 -0
- promptcloak/security.py +1 -0
- promptcloak/version.py +3 -0
- promptlatch/__init__.py +27 -0
- promptlatch/asgi.py +3 -0
- promptlatch/audit.py +34 -0
- promptlatch/cli.py +143 -0
- promptlatch/compat.py +486 -0
- promptlatch/config.py +343 -0
- promptlatch/library.py +148 -0
- promptlatch/patterns.py +114 -0
- promptlatch/proxy.py +591 -0
- promptlatch/py.typed +1 -0
- promptlatch/redaction.py +237 -0
- promptlatch/security.py +70 -0
- promptlatch/version.py +3 -0
- promptlatch-0.2.1.dist-info/METADATA +767 -0
- promptlatch-0.2.1.dist-info/RECORD +31 -0
- promptlatch-0.2.1.dist-info/WHEEL +4 -0
- promptlatch-0.2.1.dist-info/entry_points.txt +2 -0
- promptlatch-0.2.1.dist-info/licenses/LICENSE +21 -0
promptcloak/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Compatibility imports for PromptLatch 0.2."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
warnings.warn(
|
|
8
|
+
"promptcloak imports are deprecated; use promptlatch",
|
|
9
|
+
FutureWarning,
|
|
10
|
+
stacklevel=1,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from promptlatch import ( # noqa: E402
|
|
14
|
+
PromptLatch,
|
|
15
|
+
__version__,
|
|
16
|
+
redact_messages,
|
|
17
|
+
redact_params,
|
|
18
|
+
redact_payload,
|
|
19
|
+
redact_text,
|
|
20
|
+
scan_messages,
|
|
21
|
+
scan_params,
|
|
22
|
+
scan_payload,
|
|
23
|
+
scan_text,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
PromptCloak = PromptLatch
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"PromptCloak",
|
|
30
|
+
"PromptLatch",
|
|
31
|
+
"__version__",
|
|
32
|
+
"redact_messages",
|
|
33
|
+
"redact_params",
|
|
34
|
+
"redact_payload",
|
|
35
|
+
"redact_text",
|
|
36
|
+
"scan_messages",
|
|
37
|
+
"scan_params",
|
|
38
|
+
"scan_payload",
|
|
39
|
+
"scan_text",
|
|
40
|
+
]
|
promptcloak/asgi.py
ADDED
promptcloak/audit.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from promptlatch.audit import * # noqa: F403
|
promptcloak/cli.py
ADDED
promptcloak/compat.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from promptlatch.compat import * # noqa: F403
|
promptcloak/config.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from promptlatch.config import * # noqa: F403
|
promptcloak/library.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from promptlatch.library import * # noqa: F403
|
promptcloak/patterns.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from promptlatch.patterns import * # noqa: F403
|
promptcloak/proxy.py
ADDED
promptcloak/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
promptcloak/redaction.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from promptlatch.redaction import * # noqa: F403
|
promptcloak/security.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from promptlatch.security import * # noqa: F403
|
promptcloak/version.py
ADDED
promptlatch/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""PromptLatch package."""
|
|
2
|
+
|
|
3
|
+
from promptlatch.library import (
|
|
4
|
+
PromptLatch,
|
|
5
|
+
redact_messages,
|
|
6
|
+
redact_params,
|
|
7
|
+
redact_payload,
|
|
8
|
+
redact_text,
|
|
9
|
+
scan_messages,
|
|
10
|
+
scan_params,
|
|
11
|
+
scan_payload,
|
|
12
|
+
scan_text,
|
|
13
|
+
)
|
|
14
|
+
from promptlatch.version import __version__
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"PromptLatch",
|
|
18
|
+
"__version__",
|
|
19
|
+
"redact_messages",
|
|
20
|
+
"redact_params",
|
|
21
|
+
"redact_payload",
|
|
22
|
+
"redact_text",
|
|
23
|
+
"scan_messages",
|
|
24
|
+
"scan_params",
|
|
25
|
+
"scan_payload",
|
|
26
|
+
"scan_text",
|
|
27
|
+
]
|
promptlatch/asgi.py
ADDED
promptlatch/audit.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from promptlatch.config import AuditConfig
|
|
10
|
+
from promptlatch.redaction import RedactionStats
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("promptlatch")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuditLogger:
|
|
16
|
+
def __init__(self, config: AuditConfig):
|
|
17
|
+
self.config = config
|
|
18
|
+
|
|
19
|
+
def emit(self, event: str, **fields: Any) -> None:
|
|
20
|
+
if not self.config.enabled:
|
|
21
|
+
return
|
|
22
|
+
payload = {"ts": datetime.now(UTC).isoformat(), "event": event, **fields}
|
|
23
|
+
line = json.dumps(payload, sort_keys=True)
|
|
24
|
+
if self.config.file:
|
|
25
|
+
path = Path(self.config.file)
|
|
26
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
28
|
+
handle.write(line + "\n")
|
|
29
|
+
return
|
|
30
|
+
logger.info(line)
|
|
31
|
+
|
|
32
|
+
def redaction(self, path: str, stats: RedactionStats) -> None:
|
|
33
|
+
if stats.redactions:
|
|
34
|
+
self.emit("redaction", path=path, redactions=stats.redactions, rules=stats.rule_hits)
|
promptlatch/cli.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import uvicorn
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from promptlatch.config import DEFAULT_CONFIG_PATH, DEFAULT_KEY_PATH, config_template, load_settings
|
|
13
|
+
from promptlatch.proxy import create_app
|
|
14
|
+
from promptlatch.redaction import SecretRedactor
|
|
15
|
+
from promptlatch.security import encrypt_text, load_key, write_key_file, write_private_text
|
|
16
|
+
from promptlatch.version import __version__
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="PromptLatch local secret-redacting LLM proxy.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _version_callback(value: bool) -> None:
|
|
22
|
+
if value:
|
|
23
|
+
typer.echo(__version__)
|
|
24
|
+
raise typer.Exit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.callback()
|
|
28
|
+
def main(
|
|
29
|
+
version: Annotated[
|
|
30
|
+
bool,
|
|
31
|
+
typer.Option("--version", callback=_version_callback, is_eager=True, help="Show version."),
|
|
32
|
+
] = False,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Run PromptLatch."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command()
|
|
38
|
+
def serve(
|
|
39
|
+
config: Annotated[Path, typer.Option(help="Config file path.")] = DEFAULT_CONFIG_PATH,
|
|
40
|
+
host: Annotated[str | None, typer.Option(help="Override bind host.")] = None,
|
|
41
|
+
port: Annotated[int | None, typer.Option(help="Override bind port.")] = None,
|
|
42
|
+
debug_requests: Annotated[
|
|
43
|
+
bool,
|
|
44
|
+
typer.Option(
|
|
45
|
+
"--debug-requests",
|
|
46
|
+
help=(
|
|
47
|
+
"Emergency local tracing. Logs raw request bodies before redaction; "
|
|
48
|
+
"secrets may be printed."
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
] = False,
|
|
52
|
+
) -> None:
|
|
53
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
54
|
+
logging.getLogger("promptlatch").setLevel(logging.INFO)
|
|
55
|
+
settings = load_settings(config)
|
|
56
|
+
if host:
|
|
57
|
+
settings.server.host = host
|
|
58
|
+
if port:
|
|
59
|
+
settings.server.port = port
|
|
60
|
+
if debug_requests:
|
|
61
|
+
settings.server.debug_requests = True
|
|
62
|
+
uvicorn.run(create_app(settings), host=settings.server.host, port=settings.server.port)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command()
|
|
66
|
+
def version() -> None:
|
|
67
|
+
typer.echo(__version__)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command()
|
|
71
|
+
def init(
|
|
72
|
+
config: Annotated[Path, typer.Option(help="Config file to create.")] = DEFAULT_CONFIG_PATH,
|
|
73
|
+
target_base_url: Annotated[
|
|
74
|
+
str,
|
|
75
|
+
typer.Option(help="Default upstream base URL."),
|
|
76
|
+
] = "https://api.openai.com/v1",
|
|
77
|
+
target_api_key_env: Annotated[
|
|
78
|
+
str,
|
|
79
|
+
typer.Option(help="Environment variable holding upstream API key."),
|
|
80
|
+
] = "OPENAI_API_KEY",
|
|
81
|
+
force: Annotated[bool, typer.Option(help="Overwrite existing config.")] = False,
|
|
82
|
+
) -> None:
|
|
83
|
+
if config.exists() and not force:
|
|
84
|
+
raise typer.BadParameter(f"{config} already exists")
|
|
85
|
+
write_private_text(config, config_template(target_base_url, target_api_key_env))
|
|
86
|
+
typer.echo(f"created {config}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("encrypt-rules")
|
|
90
|
+
def encrypt_rules(
|
|
91
|
+
config: Annotated[Path, typer.Option(help="Config file to update.")] = DEFAULT_CONFIG_PATH,
|
|
92
|
+
key_file: Annotated[Path, typer.Option(help="AES-GCM key file.")] = DEFAULT_KEY_PATH,
|
|
93
|
+
) -> None:
|
|
94
|
+
data = yaml.safe_load(config.read_text(encoding="utf-8")) or {}
|
|
95
|
+
redaction = data.setdefault("redaction", {})
|
|
96
|
+
rules = redaction.get("rules", [])
|
|
97
|
+
if redaction.get("encrypted_rules"):
|
|
98
|
+
if rules:
|
|
99
|
+
raise typer.BadParameter("config contains both plain and encrypted redaction rules")
|
|
100
|
+
typer.echo(f"redaction rules already encrypted in {config}")
|
|
101
|
+
return
|
|
102
|
+
if not key_file.exists():
|
|
103
|
+
write_key_file(key_file)
|
|
104
|
+
encrypted = encrypt_text(yaml.safe_dump(rules, sort_keys=False), load_key(key_file))
|
|
105
|
+
redaction["encrypted"] = True
|
|
106
|
+
redaction["encrypted_rules"] = encrypted
|
|
107
|
+
redaction["rules"] = []
|
|
108
|
+
write_private_text(config, yaml.safe_dump(data, sort_keys=False))
|
|
109
|
+
typer.echo(f"encrypted redaction rules in {config}")
|
|
110
|
+
typer.echo(f"key file {key_file}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command()
|
|
114
|
+
def scan(
|
|
115
|
+
text: Annotated[str, typer.Argument(help="Text to scan and redact.")],
|
|
116
|
+
config: Annotated[Path, typer.Option(help="Config file path.")] = DEFAULT_CONFIG_PATH,
|
|
117
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
118
|
+
) -> None:
|
|
119
|
+
redactor = SecretRedactor(load_settings(config).redaction)
|
|
120
|
+
result = redactor.redact_text(text)
|
|
121
|
+
if json_output:
|
|
122
|
+
typer.echo(
|
|
123
|
+
json.dumps({"text": result.value, "stats": result.stats.__dict__}, sort_keys=True)
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
typer.echo(result.value)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command()
|
|
130
|
+
def doctor(
|
|
131
|
+
config: Annotated[Path, typer.Option(help="Config file path.")] = DEFAULT_CONFIG_PATH,
|
|
132
|
+
) -> None:
|
|
133
|
+
settings = load_settings(config)
|
|
134
|
+
checks = {
|
|
135
|
+
"config": str(config),
|
|
136
|
+
"config_exists": config.exists(),
|
|
137
|
+
"target": settings.target.default_base_url,
|
|
138
|
+
"redaction": settings.redaction.enabled,
|
|
139
|
+
"engine": settings.redaction.engine,
|
|
140
|
+
"responses_to_chat": settings.compat.responses_to_chat,
|
|
141
|
+
"telemetry": False,
|
|
142
|
+
}
|
|
143
|
+
typer.echo(json.dumps(checks, indent=2, sort_keys=True))
|