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.
@@ -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)"