silmaril-security-sdk 0.1.0__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.
- silmaril_security/sdk/__init__.py +99 -0
- silmaril_security/sdk/_utils.py +105 -0
- silmaril_security/sdk/chunking.py +41 -0
- silmaril_security/sdk/exceptions.py +83 -0
- silmaril_security/sdk/firewall.py +414 -0
- silmaril_security/sdk/hooks.py +114 -0
- silmaril_security/sdk/langchain.py +524 -0
- silmaril_security/sdk/py.typed +1 -0
- silmaril_security/sdk/types.py +59 -0
- silmaril_security_sdk-0.1.0.dist-info/METADATA +345 -0
- silmaril_security_sdk-0.1.0.dist-info/RECORD +13 -0
- silmaril_security_sdk-0.1.0.dist-info/WHEEL +4 -0
- silmaril_security_sdk-0.1.0.dist-info/licenses/LICENSE +25 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Silmaril Security Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Silmaril Firewall Python SDK."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from silmaril_security.sdk.chunking import (
|
|
8
|
+
CHARS_PER_TOKEN,
|
|
9
|
+
CHUNK_OVERLAP,
|
|
10
|
+
CHUNK_OVERLAP_CHARS,
|
|
11
|
+
CHUNK_WINDOW,
|
|
12
|
+
CHUNK_WINDOW_CHARS,
|
|
13
|
+
MAX_INPUT_CHARS,
|
|
14
|
+
MAX_INPUT_TOKENS,
|
|
15
|
+
chunk_text,
|
|
16
|
+
)
|
|
17
|
+
from silmaril_security.sdk.exceptions import (
|
|
18
|
+
APIError,
|
|
19
|
+
BatchPromptBlockedException,
|
|
20
|
+
PromptBlockedException,
|
|
21
|
+
SilmarilApiError,
|
|
22
|
+
)
|
|
23
|
+
from silmaril_security.sdk.firewall import (
|
|
24
|
+
DEFAULT_MAX_RETRIES,
|
|
25
|
+
DEFAULT_THRESHOLD,
|
|
26
|
+
DEFAULT_TIMEOUT,
|
|
27
|
+
Firewall,
|
|
28
|
+
SilmarilFirewall,
|
|
29
|
+
)
|
|
30
|
+
from silmaril_security.sdk.hooks import (
|
|
31
|
+
ALL_HOOKS,
|
|
32
|
+
DEFAULT_HOOK_THRESHOLDS,
|
|
33
|
+
DEFAULT_HOOKS,
|
|
34
|
+
FIREWALL_HOOK_TO_LABEL,
|
|
35
|
+
INPUT_HOOKS,
|
|
36
|
+
OUTPUT_HOOKS,
|
|
37
|
+
FirewallHook,
|
|
38
|
+
HookLabel,
|
|
39
|
+
prepend_hook,
|
|
40
|
+
prepend_tool_name,
|
|
41
|
+
resolve_hooks,
|
|
42
|
+
)
|
|
43
|
+
from silmaril_security.sdk.types import (
|
|
44
|
+
BlockedBatchItem,
|
|
45
|
+
BlockResult,
|
|
46
|
+
ClassifyEvent,
|
|
47
|
+
ExplainResult,
|
|
48
|
+
Prediction,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__version__ = "0.1.0"
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"ALL_HOOKS",
|
|
55
|
+
"APIError",
|
|
56
|
+
"BlockResult",
|
|
57
|
+
"BlockedBatchItem",
|
|
58
|
+
"BatchPromptBlockedException",
|
|
59
|
+
"CHARS_PER_TOKEN",
|
|
60
|
+
"CHUNK_OVERLAP",
|
|
61
|
+
"CHUNK_OVERLAP_CHARS",
|
|
62
|
+
"CHUNK_WINDOW",
|
|
63
|
+
"CHUNK_WINDOW_CHARS",
|
|
64
|
+
"ClassifyEvent",
|
|
65
|
+
"DEFAULT_HOOKS",
|
|
66
|
+
"DEFAULT_HOOK_THRESHOLDS",
|
|
67
|
+
"DEFAULT_MAX_RETRIES",
|
|
68
|
+
"DEFAULT_THRESHOLD",
|
|
69
|
+
"DEFAULT_TIMEOUT",
|
|
70
|
+
"ExplainResult",
|
|
71
|
+
"FIREWALL_HOOK_TO_LABEL",
|
|
72
|
+
"Firewall",
|
|
73
|
+
"FirewallHook",
|
|
74
|
+
"HookLabel",
|
|
75
|
+
"INPUT_HOOKS",
|
|
76
|
+
"MAX_INPUT_CHARS",
|
|
77
|
+
"MAX_INPUT_TOKENS",
|
|
78
|
+
"OUTPUT_HOOKS",
|
|
79
|
+
"Prediction",
|
|
80
|
+
"PromptBlockedException",
|
|
81
|
+
"SilmarilApiError",
|
|
82
|
+
"SilmarilFirewall",
|
|
83
|
+
"chunk_text",
|
|
84
|
+
"prepend_hook",
|
|
85
|
+
"prepend_tool_name",
|
|
86
|
+
"resolve_hooks",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def __getattr__(name: str):
|
|
91
|
+
if name == "SilmarilFirewallHandler":
|
|
92
|
+
from silmaril_security.sdk.langchain import SilmarilFirewallHandler
|
|
93
|
+
|
|
94
|
+
return SilmarilFirewallHandler
|
|
95
|
+
if name == "AsyncSilmarilFirewallHandler":
|
|
96
|
+
from silmaril_security.sdk.langchain import AsyncSilmarilFirewallHandler
|
|
97
|
+
|
|
98
|
+
return AsyncSilmarilFirewallHandler
|
|
99
|
+
raise AttributeError(f"module 'silmaril_security.sdk' has no attribute {name!r}")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Silmaril Security Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Utilities for extracting text from adapter message structures."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Sequence
|
|
8
|
+
|
|
9
|
+
_SKIP_ROLES = {"ai", "assistant"}
|
|
10
|
+
_SYSTEM_ROLES = {"system"}
|
|
11
|
+
_TOOL_ROLES = {"tool", "function"}
|
|
12
|
+
_USER_ROLES = {"human", "user"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def extract_content_text(content: Any) -> str:
|
|
16
|
+
if isinstance(content, str):
|
|
17
|
+
return content
|
|
18
|
+
if isinstance(content, list):
|
|
19
|
+
parts: list[str] = []
|
|
20
|
+
for block in content:
|
|
21
|
+
if isinstance(block, str):
|
|
22
|
+
parts.append(block)
|
|
23
|
+
elif isinstance(block, dict) and block.get("type") == "text":
|
|
24
|
+
parts.append(block.get("text", ""))
|
|
25
|
+
return " ".join(parts)
|
|
26
|
+
return ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_role(message: Any) -> str:
|
|
30
|
+
if isinstance(message, dict):
|
|
31
|
+
role = message.get("role", "")
|
|
32
|
+
return role.lower() if isinstance(role, str) else ""
|
|
33
|
+
role = getattr(message, "role", None)
|
|
34
|
+
if isinstance(role, str):
|
|
35
|
+
return role.lower()
|
|
36
|
+
msg_type = getattr(message, "type", "")
|
|
37
|
+
return msg_type.lower() if isinstance(msg_type, str) else ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_content(message: Any) -> Any:
|
|
41
|
+
if isinstance(message, dict):
|
|
42
|
+
return message.get("content", "")
|
|
43
|
+
return getattr(message, "content", "")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def find_last_user_message(messages: Sequence[Any]) -> Any | None:
|
|
47
|
+
for message in reversed(messages):
|
|
48
|
+
if get_role(message) in _USER_ROLES:
|
|
49
|
+
return message
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_last_user_text(messages: Sequence[Any]) -> str:
|
|
54
|
+
last = find_last_user_message(messages)
|
|
55
|
+
if last is None:
|
|
56
|
+
return ""
|
|
57
|
+
return extract_content_text(get_content(last)).strip()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def extract_text_from_messages(
|
|
61
|
+
messages: Sequence[Any],
|
|
62
|
+
*,
|
|
63
|
+
include_system: bool = True,
|
|
64
|
+
include_tool: bool = True,
|
|
65
|
+
) -> str:
|
|
66
|
+
parts: list[str] = []
|
|
67
|
+
for message in messages:
|
|
68
|
+
role = get_role(message)
|
|
69
|
+
if role in _SKIP_ROLES:
|
|
70
|
+
continue
|
|
71
|
+
if not include_system and role in _SYSTEM_ROLES:
|
|
72
|
+
continue
|
|
73
|
+
if not include_tool and role in _TOOL_ROLES:
|
|
74
|
+
continue
|
|
75
|
+
text = extract_content_text(get_content(message)).strip()
|
|
76
|
+
if text:
|
|
77
|
+
parts.append(text)
|
|
78
|
+
return "\n".join(parts)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def extract_text_from_prompts(prompts: Sequence[str]) -> str:
|
|
82
|
+
return "\n".join(prompt.strip() for prompt in prompts if prompt.strip())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def extract_text_from_tool_input(input_str: str) -> str:
|
|
86
|
+
return input_str.strip()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def extract_text_from_llm_result(response: Any) -> str:
|
|
90
|
+
parts: list[str] = []
|
|
91
|
+
for gen_list in getattr(response, "generations", []):
|
|
92
|
+
for gen in gen_list:
|
|
93
|
+
text = (getattr(gen, "text", "") or "").strip()
|
|
94
|
+
if text:
|
|
95
|
+
parts.append(text)
|
|
96
|
+
return "\n".join(parts)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def extract_text_from_documents(documents: Sequence[Any]) -> str:
|
|
100
|
+
parts: list[str] = []
|
|
101
|
+
for doc in documents:
|
|
102
|
+
text = (getattr(doc, "page_content", "") or "").strip()
|
|
103
|
+
if text:
|
|
104
|
+
parts.append(text)
|
|
105
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Silmaril Security Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Client-side text chunking for long firewall inputs."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
CHARS_PER_TOKEN = 4
|
|
8
|
+
|
|
9
|
+
MAX_INPUT_TOKENS = 10_240
|
|
10
|
+
CHUNK_WINDOW = 400
|
|
11
|
+
CHUNK_OVERLAP = 64
|
|
12
|
+
|
|
13
|
+
MAX_INPUT_CHARS = MAX_INPUT_TOKENS * CHARS_PER_TOKEN
|
|
14
|
+
CHUNK_WINDOW_CHARS = CHUNK_WINDOW * CHARS_PER_TOKEN
|
|
15
|
+
CHUNK_OVERLAP_CHARS = CHUNK_OVERLAP * CHARS_PER_TOKEN
|
|
16
|
+
CHUNK_STRIDE_CHARS = CHUNK_WINDOW_CHARS - CHUNK_OVERLAP_CHARS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def chunk_text(text: str) -> list[str]:
|
|
20
|
+
"""Split text into overlapping windows.
|
|
21
|
+
|
|
22
|
+
Short inputs are returned as a single-element list. Long inputs are split
|
|
23
|
+
into 400-token windows with a 64-token overlap, using the SDK-wide
|
|
24
|
+
4-characters-per-token approximation.
|
|
25
|
+
"""
|
|
26
|
+
n = len(text)
|
|
27
|
+
if n > MAX_INPUT_CHARS:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"firewall: input has ~{n // CHARS_PER_TOKEN} tokens ({n} chars); "
|
|
30
|
+
f"max is {MAX_INPUT_TOKENS} tokens ({MAX_INPUT_CHARS} chars)"
|
|
31
|
+
)
|
|
32
|
+
if n <= CHUNK_WINDOW_CHARS:
|
|
33
|
+
return [text]
|
|
34
|
+
|
|
35
|
+
chunks: list[str] = []
|
|
36
|
+
for start in range(0, n, CHUNK_STRIDE_CHARS):
|
|
37
|
+
end = start + CHUNK_WINDOW_CHARS
|
|
38
|
+
chunks.append(text[start:end])
|
|
39
|
+
if end >= n:
|
|
40
|
+
break
|
|
41
|
+
return chunks
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Silmaril Security Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Exceptions raised by the Silmaril Firewall SDK."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from silmaril_security.sdk.hooks import HookLabel
|
|
10
|
+
from silmaril_security.sdk.types import BlockedBatchItem, BlockResult
|
|
11
|
+
|
|
12
|
+
_MAX_PROMPT_DISPLAY_LEN = 100
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SilmarilApiError(Exception):
|
|
16
|
+
"""Raised when the firewall API returns a non-success HTTP status."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, *, status: int, status_text: str, body: str) -> None:
|
|
19
|
+
self.status = status
|
|
20
|
+
self.status_text = status_text
|
|
21
|
+
self.body = body
|
|
22
|
+
super().__init__(f"Silmaril API error {status} {status_text}: {body}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
APIError = SilmarilApiError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PromptBlockedException(Exception):
|
|
29
|
+
"""Raised when a single classification meets or exceeds its threshold."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
score: float,
|
|
35
|
+
threshold: float,
|
|
36
|
+
prompt_text: str,
|
|
37
|
+
hook: HookLabel = HookLabel.UNKNOWN,
|
|
38
|
+
tool_name: str | None = None,
|
|
39
|
+
result: BlockResult | None = None,
|
|
40
|
+
run_id: UUID | str | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.score = score
|
|
43
|
+
self.threshold = threshold
|
|
44
|
+
self.prompt_text = prompt_text
|
|
45
|
+
self.hook = hook
|
|
46
|
+
self.tool_name = tool_name
|
|
47
|
+
self.result = result
|
|
48
|
+
self.run_id = run_id
|
|
49
|
+
super().__init__(str(self))
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
truncated = self.prompt_text
|
|
53
|
+
if len(truncated) > _MAX_PROMPT_DISPLAY_LEN:
|
|
54
|
+
truncated = truncated[:_MAX_PROMPT_DISPLAY_LEN] + "..."
|
|
55
|
+
return (
|
|
56
|
+
"Prompt blocked by Silmaril Firewall "
|
|
57
|
+
f"(score={self.score:.4f}, threshold={self.threshold:.4f}): "
|
|
58
|
+
f"{truncated!r}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BatchPromptBlockedException(Exception):
|
|
63
|
+
"""Raised when one or more batch items meet or exceed their threshold."""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
blocked: list[BlockedBatchItem],
|
|
69
|
+
results: list[BlockResult],
|
|
70
|
+
) -> None:
|
|
71
|
+
self.blocked = blocked
|
|
72
|
+
self.results = results
|
|
73
|
+
super().__init__(str(self))
|
|
74
|
+
|
|
75
|
+
def __str__(self) -> str:
|
|
76
|
+
if len(self.blocked) == 1:
|
|
77
|
+
item = self.blocked[0]
|
|
78
|
+
return (
|
|
79
|
+
"Batch prompt blocked by Silmaril Firewall "
|
|
80
|
+
f"at index {item.index} "
|
|
81
|
+
f"(score={item.result.score:.4f}, threshold={item.result.threshold:.4f})"
|
|
82
|
+
)
|
|
83
|
+
return f"Batch prompts blocked by Silmaril Firewall ({len(self.blocked)} items)"
|