Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "arcjet"
version = "0.6.1"
version = "0.7.0a1"
description = "Arcjet Python SDK. Runtime security for AI applications. Prompt injection detection, bot protection, rate limiting, sensitive data detection, and WAF for Python apps."
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -81,6 +81,7 @@ python_files = ["test_*.py"]
omit = [
# Generated protobuf code
"src/arcjet/proto/*",
"src/arcjet/guard/proto/*",
# Generated by witgen (uv run python -m tools.witgen)
"src/arcjet/_analyze/__init__.py",
"src/arcjet/_analyze/_types.py",
Expand All @@ -97,6 +98,7 @@ show_missing = true
exclude = [
# Generated code
"src/arcjet/proto",
"src/arcjet/guard/proto",
"src/arcjet/_analyze",
# TODO: re-enable once we fix the issues
"examples",
Expand All @@ -110,6 +112,8 @@ select = [

[tool.pyright]
exclude = [
# Generated protobuf code
"src/arcjet/guard/proto",
"tests/mocked/conftest.py",
"tests/mocked/test_cache.py",
"tests/mocked/test_client_sync.py",
Expand All @@ -127,6 +131,8 @@ exclude-newer = "7 days"

[tool.ty.src]
exclude = [
# Generated protobuf code
"src/arcjet/guard/proto",
# TODO: re-enable once we fix the issues
"examples",
"tests/mocked/conftest.py",
Expand Down
120 changes: 120 additions & 0 deletions src/arcjet/guard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Arcjet Guard SDK — AI guardrails for rate limiting, prompt injection
detection, and sensitive info detection.

Public API
----------

**Types** (from ``types``)::

Conclusion, Reason, Mode, Decision,
RuleResult, RuleResultTokenBucket, RuleResultFixedWindow,
RuleResultSlidingWindow, RuleResultPromptInjection,
RuleResultSensitiveInfo, RuleResultNotRun,
RuleResultError, RuleResultUnknown

**Rule classes** (from ``rules``)::

TokenBucket, FixedWindow, SlidingWindow,
DetectPromptInjection, LocalDetectSensitiveInfo, LocalCustomRule

**Concrete rule input types** (from ``rules``)::

TokenBucketWithInput, FixedWindowWithInput, SlidingWindowWithInput,
PromptInjectionWithInput, SensitiveInfoWithInput,
LocalCustomWithInput,
RuleWithInput (union of all)

**Configured rule union** (from ``rules``)::

RuleWithConfig (union of all configured rule types)

**Client factories** (from ``client``)::

launch_arcjet, launch_arcjet_sync
ArcjetGuard, ArcjetGuardSync
"""

from .client import (
ArcjetGuard,
ArcjetGuardSync,
launch_arcjet,
launch_arcjet_sync,
)
from .rules import (
DetectPromptInjection,
FixedWindow,
FixedWindowWithInput,
LocalCustomRule,
LocalCustomWithInput,
LocalDetectSensitiveInfo,
PromptInjectionWithInput,
RuleWithConfig,
RuleWithInput,
SensitiveInfoWithInput,
SlidingWindow,
SlidingWindowWithInput,
TokenBucket,
TokenBucketWithInput,
TypedCustomResult,
)
from .types import (
SENSITIVE_INFO_ENTITY_TYPES,
Conclusion,
CustomEvaluateResult,
Decision,
Mode,
Reason,
RuleResult,
RuleResultCustom,
RuleResultError,
RuleResultFixedWindow,
RuleResultNotRun,
RuleResultPromptInjection,
RuleResultSensitiveInfo,
RuleResultSlidingWindow,
RuleResultTokenBucket,
RuleResultUnknown,
)

__all__ = [
# Types
"Conclusion",
"CustomEvaluateResult",
"Decision",
"Mode",
"Reason",
"RuleResult",
"RuleResultCustom",
"RuleResultError",
"RuleResultFixedWindow",
"RuleResultNotRun",
"RuleResultPromptInjection",
"RuleResultSensitiveInfo",
"RuleResultSlidingWindow",
"RuleResultTokenBucket",
"RuleResultUnknown",
"SENSITIVE_INFO_ENTITY_TYPES",
# Rule classes
"DetectPromptInjection",
"FixedWindow",
"LocalCustomRule",
"LocalDetectSensitiveInfo",
"SlidingWindow",
"TokenBucket",
# Concrete input types
"FixedWindowWithInput",
"LocalCustomWithInput",
"PromptInjectionWithInput",
"SensitiveInfoWithInput",
"SlidingWindowWithInput",
"TokenBucketWithInput",
"TypedCustomResult",
# Union aliases
"RuleWithConfig",
"RuleWithInput",
# Client factories
"ArcjetGuard",
"ArcjetGuardSync",
"launch_arcjet",
"launch_arcjet_sync",
]
117 changes: 117 additions & 0 deletions src/arcjet/guard/_local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Local WASM-based rule evaluation for ``arcjet.guard``.

Evaluates sensitive info detection rules via the arcjet-analyze WASM
component. The raw text never leaves the SDK — only a SHA-256 hash is sent
to the server alongside the locally-computed result.
"""

from __future__ import annotations

import hashlib
import time
from dataclasses import dataclass

from arcjet._logging import logger


def _get_component(): # noqa: ANN202
"""Return the shared AnalyzeComponent singleton, or ``None``."""
# Import lazily to avoid hard dep on WASM at import time.
from arcjet._local import _get_component

return _get_component()


def _to_wasm_entity(specifier: str): # noqa: ANN202
"""Convert an entity type string to a WASM entity value."""
from arcjet._local import _to_wasm_entity

return _to_wasm_entity(specifier)


def _detected_entity_type_str(entity: object) -> str:
"""Extract a string type name from a ``DetectedSensitiveInfoEntity``."""
from arcjet._local import _detected_entity_type_str

return _detected_entity_type_str(entity) # type: ignore[arg-type]


def hash_text(text: str) -> str:
"""Return a SHA-256 hex digest of *text*."""
return hashlib.sha256(text.encode("utf-8")).hexdigest()


@dataclass(frozen=True, slots=True)
class LocalSensitiveInfoResult:
"""Result of running sensitive info detection locally via WASM."""

conclusion: str
detected_entity_types: list[str]
elapsed_ms: int


@dataclass(frozen=True, slots=True)
class LocalSensitiveInfoError:
"""Indicates a local evaluation error."""

message: str
code: str


def evaluate_sensitive_info_locally(
text: str,
*,
allow: tuple[str, ...] = (),
deny: tuple[str, ...] = (),
) -> LocalSensitiveInfoResult | LocalSensitiveInfoError | None:
"""Run sensitive info detection via WASM.

Returns a :class:`LocalSensitiveInfoResult` on success,
a :class:`LocalSensitiveInfoError` on failure, or ``None`` if the
WASM component is unavailable.
"""
from arcjet._analyze import (
SensitiveInfoConfig,
SensitiveInfoEntitiesAllow,
SensitiveInfoEntitiesDeny,
)

component = _get_component()
if component is None:
return None

if not text:
return None

if allow:
wasm_entities = [_to_wasm_entity(e) for e in allow]
entities_cfg = SensitiveInfoEntitiesAllow(entities=wasm_entities)
elif deny:
wasm_entities = [_to_wasm_entity(e) for e in deny]
entities_cfg = SensitiveInfoEntitiesDeny(entities=wasm_entities)
else:
entities_cfg = SensitiveInfoEntitiesDeny(entities=[])

wasm_config = SensitiveInfoConfig(
entities=entities_cfg,
context_window_size=None,
skip_custom_detect=True,
)

start = time.monotonic()
try:
result = component.detect_sensitive_info(text, wasm_config)
except Exception as exc:
logger.debug("guard: local sensitive info detection error: %s", exc)
return LocalSensitiveInfoError(message=str(exc), code="WASM_ERROR")
elapsed_ms = int((time.monotonic() - start) * 1000)

denied_types = [_detected_entity_type_str(e) for e in result.denied]
has_deny = len(denied_types) > 0
conclusion = "DENY" if has_deny else "ALLOW"

return LocalSensitiveInfoResult(
conclusion=conclusion,
detected_entity_types=denied_types,
elapsed_ms=elapsed_ms,
)
Loading
Loading