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.
@@ -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
@@ -0,0 +1,3 @@
1
+ from promptlatch.asgi import app
2
+
3
+ __all__ = ["app"]
promptcloak/audit.py ADDED
@@ -0,0 +1 @@
1
+ from promptlatch.audit import * # noqa: F403
promptcloak/cli.py ADDED
@@ -0,0 +1,3 @@
1
+ from promptlatch.cli import app
2
+
3
+ __all__ = ["app"]
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
@@ -0,0 +1 @@
1
+ from promptlatch.patterns import * # noqa: F403
promptcloak/proxy.py ADDED
@@ -0,0 +1,3 @@
1
+ from promptlatch.proxy import create_app
2
+
3
+ __all__ = ["create_app"]
promptcloak/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+ from promptlatch.redaction import * # noqa: F403
@@ -0,0 +1 @@
1
+ from promptlatch.security import * # noqa: F403
promptcloak/version.py ADDED
@@ -0,0 +1,3 @@
1
+ from promptlatch.version import __version__
2
+
3
+ __all__ = ["__version__"]
@@ -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
@@ -0,0 +1,3 @@
1
+ from promptlatch.proxy import create_app
2
+
3
+ app = create_app()
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))