shieldops-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.
- shieldops_sdk/__init__.py +50 -0
- shieldops_sdk/_policy/__init__.py +41 -0
- shieldops_sdk/_policy/_defaults.py +33 -0
- shieldops_sdk/_response.py +72 -0
- shieldops_sdk/async_client.py +83 -0
- shieldops_sdk/client.py +85 -0
- shieldops_sdk/config.py +95 -0
- shieldops_sdk/exceptions.py +85 -0
- shieldops_sdk/experimental/__init__.py +22 -0
- shieldops_sdk/experimental/autogen.py +114 -0
- shieldops_sdk/experimental/openai_agents.py +96 -0
- shieldops_sdk/integrations/__init__.py +3 -0
- shieldops_sdk/integrations/crewai.py +83 -0
- shieldops_sdk/integrations/langchain.py +91 -0
- shieldops_sdk/integrations/llamaindex.py +86 -0
- shieldops_sdk/interceptor.py +217 -0
- shieldops_sdk/models.py +120 -0
- shieldops_sdk/resources/__init__.py +31 -0
- shieldops_sdk/resources/agents.py +90 -0
- shieldops_sdk/resources/investigations.py +137 -0
- shieldops_sdk/resources/remediations.py +218 -0
- shieldops_sdk/resources/security.py +168 -0
- shieldops_sdk/resources/vulnerabilities.py +253 -0
- shieldops_sdk/telemetry.py +196 -0
- shieldops_sdk-0.1.0.dist-info/METADATA +221 -0
- shieldops_sdk-0.1.0.dist-info/RECORD +28 -0
- shieldops_sdk-0.1.0.dist-info/WHEEL +4 -0
- shieldops_sdk-0.1.0.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""ShieldOps SDK -- AI Security Control Plane SDK.
|
|
2
|
+
|
|
3
|
+
Intercept and govern AI agent tool calls with one line of code.
|
|
4
|
+
|
|
5
|
+
Quick start::
|
|
6
|
+
|
|
7
|
+
from shieldops_sdk import ShieldOpsClient, ShieldOpsInterceptor, ShieldOpsConfig
|
|
8
|
+
|
|
9
|
+
# API client for the ShieldOps platform
|
|
10
|
+
client = ShieldOpsClient(api_key="sk-...")
|
|
11
|
+
|
|
12
|
+
# Framework-agnostic tool call interceptor
|
|
13
|
+
config = ShieldOpsConfig(api_key="sk-...", mode="enforce")
|
|
14
|
+
interceptor = ShieldOpsInterceptor(config)
|
|
15
|
+
decision = interceptor.check("my_tool", {"arg": "value"})
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from shieldops_sdk.async_client import AsyncShieldOpsClient
|
|
21
|
+
from shieldops_sdk.client import ShieldOpsClient
|
|
22
|
+
from shieldops_sdk.config import SDKMode, ShieldOpsConfig
|
|
23
|
+
from shieldops_sdk.exceptions import (
|
|
24
|
+
AuthenticationError,
|
|
25
|
+
NotFoundError,
|
|
26
|
+
RateLimitError,
|
|
27
|
+
ShieldOpsConnectionError,
|
|
28
|
+
ShieldOpsDeniedError,
|
|
29
|
+
ShieldOpsError,
|
|
30
|
+
ValidationError,
|
|
31
|
+
)
|
|
32
|
+
from shieldops_sdk.interceptor import Decision, ShieldOpsInterceptor, ToolCall
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AsyncShieldOpsClient",
|
|
36
|
+
"AuthenticationError",
|
|
37
|
+
"Decision",
|
|
38
|
+
"NotFoundError",
|
|
39
|
+
"RateLimitError",
|
|
40
|
+
"SDKMode",
|
|
41
|
+
"ShieldOpsClient",
|
|
42
|
+
"ShieldOpsConfig",
|
|
43
|
+
"ShieldOpsConnectionError",
|
|
44
|
+
"ShieldOpsDeniedError",
|
|
45
|
+
"ShieldOpsError",
|
|
46
|
+
"ShieldOpsInterceptor",
|
|
47
|
+
"ToolCall",
|
|
48
|
+
"ValidationError",
|
|
49
|
+
]
|
|
50
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Private policy module — default pattern catalogues and merge helpers.
|
|
2
|
+
|
|
3
|
+
The leading underscore makes this package private. It is not part of the
|
|
4
|
+
public SDK API and may be reorganised between minor releases. Public clients
|
|
5
|
+
should configure policy via ``ShieldOpsConfig.extra_blocked_patterns`` and
|
|
6
|
+
``ShieldOpsConfig.extra_high_risk_patterns`` rather than importing from here.
|
|
7
|
+
|
|
8
|
+
The helpers centralise the "effective policy set" computation so future PRs
|
|
9
|
+
(mode/telemetry split, callbacks) can reuse one merge implementation rather
|
|
10
|
+
than duplicating ``defaults | extras`` in multiple call sites.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from shieldops_sdk._policy._defaults import (
|
|
18
|
+
DEFAULT_BLOCKED_PATTERNS,
|
|
19
|
+
DEFAULT_HIGH_RISK_PATTERNS,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from shieldops_sdk.config import ShieldOpsConfig
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def effective_blocked_patterns(config: ShieldOpsConfig) -> set[str]:
|
|
27
|
+
"""Return a fresh set of blocked patterns: defaults ∪ config.extra_blocked_patterns."""
|
|
28
|
+
return set(DEFAULT_BLOCKED_PATTERNS) | set(config.extra_blocked_patterns)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def effective_high_risk_patterns(config: ShieldOpsConfig) -> set[str]:
|
|
32
|
+
"""Return a fresh set of high-risk patterns: defaults ∪ config.extra_high_risk_patterns."""
|
|
33
|
+
return set(DEFAULT_HIGH_RISK_PATTERNS) | set(config.extra_high_risk_patterns)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"DEFAULT_BLOCKED_PATTERNS",
|
|
38
|
+
"DEFAULT_HIGH_RISK_PATTERNS",
|
|
39
|
+
"effective_blocked_patterns",
|
|
40
|
+
"effective_high_risk_patterns",
|
|
41
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Default pattern catalogues for the ShieldOps interceptor.
|
|
2
|
+
|
|
3
|
+
These are ``frozenset`` so the module-level defaults cannot be mutated by
|
|
4
|
+
client code. The interceptor copies them into per-instance ``set`` attributes
|
|
5
|
+
at construction time, preserving the existing pattern of
|
|
6
|
+
``interceptor._blocked_tools.update(...)`` for advanced post-construct
|
|
7
|
+
configuration.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
DEFAULT_BLOCKED_PATTERNS: frozenset[str] = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"delete_database",
|
|
15
|
+
"drop_table",
|
|
16
|
+
"modify_iam_root",
|
|
17
|
+
"rm_rf",
|
|
18
|
+
"format_disk",
|
|
19
|
+
"disable_firewall",
|
|
20
|
+
"delete_backup",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
DEFAULT_HIGH_RISK_PATTERNS: frozenset[str] = frozenset(
|
|
25
|
+
{
|
|
26
|
+
"execute_command",
|
|
27
|
+
"run_shell",
|
|
28
|
+
"modify_security_group",
|
|
29
|
+
"change_iam_policy",
|
|
30
|
+
"create_user",
|
|
31
|
+
"rotate_credentials",
|
|
32
|
+
}
|
|
33
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Shared HTTP response handling for sync and async clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from shieldops_sdk.exceptions import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ServerError,
|
|
12
|
+
ShieldOpsError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def handle_response(response: httpx.Response) -> None:
|
|
18
|
+
"""Raise the appropriate SDK exception for non-2xx responses.
|
|
19
|
+
|
|
20
|
+
This is intentionally a plain function so both sync and async
|
|
21
|
+
resource classes can reuse it without duplication.
|
|
22
|
+
"""
|
|
23
|
+
if response.is_success:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
status = response.status_code
|
|
27
|
+
|
|
28
|
+
# Try to extract a detail message from the JSON body
|
|
29
|
+
detail = ""
|
|
30
|
+
try:
|
|
31
|
+
body = response.json()
|
|
32
|
+
detail = body.get("detail", "") if isinstance(body, dict) else ""
|
|
33
|
+
except Exception: # noqa: BLE001
|
|
34
|
+
detail = response.text[:200] if response.text else ""
|
|
35
|
+
|
|
36
|
+
if status in (401, 403):
|
|
37
|
+
raise AuthenticationError(detail or "Authentication failed")
|
|
38
|
+
|
|
39
|
+
if status == 404:
|
|
40
|
+
raise NotFoundError(detail or "Resource not found")
|
|
41
|
+
|
|
42
|
+
if status == 422:
|
|
43
|
+
raise ValidationError(detail or "Validation error")
|
|
44
|
+
|
|
45
|
+
if status == 429:
|
|
46
|
+
retry_after: int | None = None
|
|
47
|
+
raw = response.headers.get("Retry-After")
|
|
48
|
+
if raw is not None:
|
|
49
|
+
try:
|
|
50
|
+
retry_after = int(raw)
|
|
51
|
+
except ValueError:
|
|
52
|
+
pass
|
|
53
|
+
# Also try the JSON body which ShieldOps returns
|
|
54
|
+
if retry_after is None:
|
|
55
|
+
try:
|
|
56
|
+
body = response.json()
|
|
57
|
+
retry_after = body.get("retry_after")
|
|
58
|
+
except Exception: # noqa: BLE001
|
|
59
|
+
pass
|
|
60
|
+
raise RateLimitError(
|
|
61
|
+
detail or "Rate limit exceeded",
|
|
62
|
+
retry_after=retry_after,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if status >= 500:
|
|
66
|
+
raise ServerError(
|
|
67
|
+
detail or "Internal server error",
|
|
68
|
+
status_code=status,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Catch-all for other 4xx
|
|
72
|
+
raise ShieldOpsError(detail or f"Request failed with status {status}", status_code=status)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Asynchronous ShieldOps API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from shieldops_sdk._response import handle_response
|
|
10
|
+
from shieldops_sdk.resources.agents import AsyncAgentsResource
|
|
11
|
+
from shieldops_sdk.resources.investigations import AsyncInvestigationsResource
|
|
12
|
+
from shieldops_sdk.resources.remediations import AsyncRemediationsResource
|
|
13
|
+
from shieldops_sdk.resources.security import AsyncSecurityResource
|
|
14
|
+
from shieldops_sdk.resources.vulnerabilities import AsyncVulnerabilitiesResource
|
|
15
|
+
|
|
16
|
+
_DEFAULT_BASE_URL = "http://localhost:8000/api/v1"
|
|
17
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncShieldOpsClient:
|
|
21
|
+
"""Async client for the ShieldOps REST API.
|
|
22
|
+
|
|
23
|
+
Usage::
|
|
24
|
+
|
|
25
|
+
async with AsyncShieldOpsClient(api_key="sk-...") as client:
|
|
26
|
+
invs = await client.investigations.list(limit=10)
|
|
27
|
+
for inv in invs.items:
|
|
28
|
+
print(inv.investigation_id, inv.status)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
35
|
+
api_key: str | None = None,
|
|
36
|
+
token: str | None = None,
|
|
37
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
38
|
+
) -> None:
|
|
39
|
+
headers: dict[str, str] = {"User-Agent": "shieldops-sdk/1.0.0"}
|
|
40
|
+
if api_key:
|
|
41
|
+
headers["X-API-Key"] = api_key
|
|
42
|
+
elif token:
|
|
43
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
44
|
+
|
|
45
|
+
self._http = httpx.AsyncClient(
|
|
46
|
+
base_url=base_url,
|
|
47
|
+
headers=headers,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Resource namespaces
|
|
52
|
+
self.investigations = AsyncInvestigationsResource(self._http)
|
|
53
|
+
self.remediations = AsyncRemediationsResource(self._http)
|
|
54
|
+
self.security = AsyncSecurityResource(self._http)
|
|
55
|
+
self.vulnerabilities = AsyncVulnerabilitiesResource(self._http)
|
|
56
|
+
self.agents = AsyncAgentsResource(self._http)
|
|
57
|
+
|
|
58
|
+
# -- Async context manager --------------------------------------------
|
|
59
|
+
|
|
60
|
+
async def __aenter__(self) -> AsyncShieldOpsClient:
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
async def __aexit__(
|
|
64
|
+
self,
|
|
65
|
+
exc_type: type[BaseException] | None,
|
|
66
|
+
exc_val: BaseException | None,
|
|
67
|
+
exc_tb: Any,
|
|
68
|
+
) -> None:
|
|
69
|
+
await self.close()
|
|
70
|
+
|
|
71
|
+
async def close(self) -> None:
|
|
72
|
+
"""Close the underlying HTTP transport."""
|
|
73
|
+
await self._http.aclose()
|
|
74
|
+
|
|
75
|
+
# -- Convenience methods ----------------------------------------------
|
|
76
|
+
|
|
77
|
+
async def health(self) -> dict[str, Any]:
|
|
78
|
+
"""Check the API health endpoint."""
|
|
79
|
+
base = str(self._http.base_url)
|
|
80
|
+
root = base.rsplit("/api/", 1)[0] if "/api/" in base else base
|
|
81
|
+
resp = await self._http.get(f"{root}/health")
|
|
82
|
+
handle_response(resp)
|
|
83
|
+
return resp.json()
|
shieldops_sdk/client.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Synchronous ShieldOps API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from shieldops_sdk._response import handle_response
|
|
10
|
+
from shieldops_sdk.resources.agents import AgentsResource
|
|
11
|
+
from shieldops_sdk.resources.investigations import InvestigationsResource
|
|
12
|
+
from shieldops_sdk.resources.remediations import RemediationsResource
|
|
13
|
+
from shieldops_sdk.resources.security import SecurityResource
|
|
14
|
+
from shieldops_sdk.resources.vulnerabilities import VulnerabilitiesResource
|
|
15
|
+
|
|
16
|
+
_DEFAULT_BASE_URL = "http://localhost:8000/api/v1"
|
|
17
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ShieldOpsClient:
|
|
21
|
+
"""Synchronous client for the ShieldOps REST API.
|
|
22
|
+
|
|
23
|
+
Usage::
|
|
24
|
+
|
|
25
|
+
with ShieldOpsClient(api_key="sk-...") as client:
|
|
26
|
+
invs = client.investigations.list(limit=10)
|
|
27
|
+
for inv in invs.items:
|
|
28
|
+
print(inv.investigation_id, inv.status)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
35
|
+
api_key: str | None = None,
|
|
36
|
+
token: str | None = None,
|
|
37
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
38
|
+
) -> None:
|
|
39
|
+
headers: dict[str, str] = {"User-Agent": "shieldops-sdk/1.0.0"}
|
|
40
|
+
if api_key:
|
|
41
|
+
headers["X-API-Key"] = api_key
|
|
42
|
+
elif token:
|
|
43
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
44
|
+
|
|
45
|
+
self._http = httpx.Client(
|
|
46
|
+
base_url=base_url,
|
|
47
|
+
headers=headers,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Resource namespaces
|
|
52
|
+
self.investigations = InvestigationsResource(self._http)
|
|
53
|
+
self.remediations = RemediationsResource(self._http)
|
|
54
|
+
self.security = SecurityResource(self._http)
|
|
55
|
+
self.vulnerabilities = VulnerabilitiesResource(self._http)
|
|
56
|
+
self.agents = AgentsResource(self._http)
|
|
57
|
+
|
|
58
|
+
# -- Context manager --------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def __enter__(self) -> ShieldOpsClient:
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(
|
|
64
|
+
self,
|
|
65
|
+
exc_type: type[BaseException] | None,
|
|
66
|
+
exc_val: BaseException | None,
|
|
67
|
+
exc_tb: Any,
|
|
68
|
+
) -> None:
|
|
69
|
+
self.close()
|
|
70
|
+
|
|
71
|
+
def close(self) -> None:
|
|
72
|
+
"""Close the underlying HTTP transport."""
|
|
73
|
+
self._http.close()
|
|
74
|
+
|
|
75
|
+
# -- Convenience methods ----------------------------------------------
|
|
76
|
+
|
|
77
|
+
def health(self) -> dict[str, Any]:
|
|
78
|
+
"""Check the API health endpoint."""
|
|
79
|
+
# Health lives outside the /api/v1 prefix, so use an absolute URL.
|
|
80
|
+
base = str(self._http.base_url)
|
|
81
|
+
# Strip the API version path to reach the root.
|
|
82
|
+
root = base.rsplit("/api/", 1)[0] if "/api/" in base else base
|
|
83
|
+
resp = self._http.get(f"{root}/health")
|
|
84
|
+
handle_response(resp)
|
|
85
|
+
return resp.json()
|
shieldops_sdk/config.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""ShieldOps SDK configuration — loaded from constructor args or environment variables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SDKMode(str, Enum):
|
|
12
|
+
"""SDK enforcement mode."""
|
|
13
|
+
|
|
14
|
+
AUDIT = "audit"
|
|
15
|
+
ENFORCE = "enforce"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SDKTelemetry(str, Enum):
|
|
19
|
+
"""SDK telemetry destination.
|
|
20
|
+
|
|
21
|
+
Separate from :class:`SDKMode` — mode controls block-vs-audit on policy
|
|
22
|
+
violations; telemetry controls *where* records of those decisions go.
|
|
23
|
+
|
|
24
|
+
- ``LOCAL``: keep all events in-process, no network at all.
|
|
25
|
+
- ``REMOTE``: send to the ShieldOps backend (requires ``api_key``).
|
|
26
|
+
- ``OTLP``: send to the user's OpenTelemetry collector.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
LOCAL = "local"
|
|
30
|
+
REMOTE = "remote"
|
|
31
|
+
OTLP = "otlp"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ShieldOpsConfig(BaseModel):
|
|
35
|
+
"""Configuration for the ShieldOps SDK.
|
|
36
|
+
|
|
37
|
+
Values can be provided directly or read from environment variables:
|
|
38
|
+
- ``SHIELDOPS_API_KEY``
|
|
39
|
+
- ``SHIELDOPS_ENDPOINT``
|
|
40
|
+
- ``SHIELDOPS_MODE``
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
api_key: ShieldOps API key for authentication.
|
|
44
|
+
endpoint: ShieldOps API base URL.
|
|
45
|
+
mode: Operating mode -- ``audit`` logs without blocking, ``enforce`` blocks risky calls.
|
|
46
|
+
timeout: HTTP request timeout in seconds.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
api_key: str = Field(default="")
|
|
50
|
+
endpoint: str = Field(default="https://api.shieldops.io")
|
|
51
|
+
mode: SDKMode = SDKMode.AUDIT
|
|
52
|
+
telemetry: SDKTelemetry = Field(
|
|
53
|
+
default=SDKTelemetry.LOCAL,
|
|
54
|
+
description=(
|
|
55
|
+
"Where intercept records are sent. LOCAL = in-process only "
|
|
56
|
+
"(default, no network). REMOTE = POST to ShieldOps backend "
|
|
57
|
+
"(requires api_key). OTLP = export via OpenTelemetry collector."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
timeout: float = Field(default=5.0, ge=0.1)
|
|
61
|
+
extra_blocked_patterns: set[str] = Field(
|
|
62
|
+
default_factory=set,
|
|
63
|
+
description=(
|
|
64
|
+
"Additional tool-name patterns to deny on top of the SDK defaults. "
|
|
65
|
+
"Merged with shieldops_sdk._policy defaults at interceptor construction."
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
extra_high_risk_patterns: set[str] = Field(
|
|
69
|
+
default_factory=set,
|
|
70
|
+
description=(
|
|
71
|
+
"Additional tool-name patterns to flag as high-risk on top of the "
|
|
72
|
+
"SDK defaults. Merged with shieldops_sdk._policy defaults at "
|
|
73
|
+
"interceptor construction."
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def model_post_init(self, __context: object) -> None:
|
|
78
|
+
"""Populate unset fields from environment variables."""
|
|
79
|
+
if not self.api_key:
|
|
80
|
+
self.api_key = os.environ.get("SHIELDOPS_API_KEY", "")
|
|
81
|
+
if self.endpoint == "https://api.shieldops.io":
|
|
82
|
+
env_endpoint = os.environ.get("SHIELDOPS_ENDPOINT", "")
|
|
83
|
+
if env_endpoint:
|
|
84
|
+
self.endpoint = env_endpoint
|
|
85
|
+
env_mode = os.environ.get("SHIELDOPS_MODE", "")
|
|
86
|
+
if env_mode and env_mode.lower() in ("audit", "enforce"):
|
|
87
|
+
self.mode = SDKMode(env_mode.lower())
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_enforce(self) -> bool:
|
|
91
|
+
return self.mode == SDKMode.ENFORCE
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def is_audit(self) -> bool:
|
|
95
|
+
return self.mode == SDKMode.AUDIT
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""ShieldOps SDK exceptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ShieldOpsError(Exception):
|
|
7
|
+
"""Base exception for all SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.message = message
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthenticationError(ShieldOpsError):
|
|
16
|
+
"""Invalid or expired API credentials (401/403)."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, message: str = "Authentication failed") -> None:
|
|
19
|
+
super().__init__(message, status_code=401)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotFoundError(ShieldOpsError):
|
|
23
|
+
"""Resource not found (404)."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str = "Resource not found") -> None:
|
|
26
|
+
super().__init__(message, status_code=404)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RateLimitError(ShieldOpsError):
|
|
30
|
+
"""Rate limit exceeded (429)."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
message: str = "Rate limit exceeded",
|
|
35
|
+
retry_after: int | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
super().__init__(message, status_code=429)
|
|
38
|
+
self.retry_after = retry_after
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ValidationError(ShieldOpsError):
|
|
42
|
+
"""Request validation failed (422)."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, message: str = "Validation error") -> None:
|
|
45
|
+
super().__init__(message, status_code=422)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ServerError(ShieldOpsError):
|
|
49
|
+
"""Server-side error (5xx)."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
message: str = "Internal server error",
|
|
54
|
+
status_code: int = 500,
|
|
55
|
+
) -> None:
|
|
56
|
+
super().__init__(message, status_code=status_code)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ShieldOpsDeniedError(ShieldOpsError):
|
|
60
|
+
"""Raised in enforce mode when a tool call is denied by policy.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
tool_name: The tool that was denied.
|
|
64
|
+
reasons: List of policy violation reasons.
|
|
65
|
+
risk_score: The computed risk score for the tool call.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
tool_name: str = "",
|
|
71
|
+
reasons: list[str] | None = None,
|
|
72
|
+
risk_score: float = 0.0,
|
|
73
|
+
) -> None:
|
|
74
|
+
self.tool_name = tool_name
|
|
75
|
+
self.reasons = reasons or []
|
|
76
|
+
self.risk_score = risk_score
|
|
77
|
+
detail = f"Tool '{tool_name}' denied: {', '.join(self.reasons)}"
|
|
78
|
+
super().__init__(detail, status_code=403)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ShieldOpsConnectionError(ShieldOpsError):
|
|
82
|
+
"""Raised when the SDK cannot reach the ShieldOps API."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, message: str = "Failed to connect to ShieldOps API") -> None:
|
|
85
|
+
super().__init__(message, status_code=None)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""ShieldOps SDK experimental namespace.
|
|
2
|
+
|
|
3
|
+
Modules under ``shieldops_sdk.experimental`` provide integrations for AI agent
|
|
4
|
+
frameworks whose APIs are still in flux. Their public surface may change in any
|
|
5
|
+
minor SDK release without a deprecation cycle — pin a specific SDK version if
|
|
6
|
+
you depend on these.
|
|
7
|
+
|
|
8
|
+
Stable integrations live under ``shieldops_sdk.integrations``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import warnings
|
|
14
|
+
|
|
15
|
+
warnings.warn(
|
|
16
|
+
"shieldops_sdk.experimental contains unstable integrations whose surface "
|
|
17
|
+
"may change without notice between minor releases. Pin your SDK version "
|
|
18
|
+
"if you depend on these. Stable integrations live under "
|
|
19
|
+
"shieldops_sdk.integrations.",
|
|
20
|
+
UserWarning,
|
|
21
|
+
stacklevel=2,
|
|
22
|
+
)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""ShieldOps Microsoft AutoGen integration (experimental).
|
|
2
|
+
|
|
3
|
+
Wraps an AutoGen agent so its ``execute_function`` calls flow through the
|
|
4
|
+
ShieldOps interceptor. In ``enforce`` mode a denied call raises
|
|
5
|
+
``ShieldOpsDeniedError`` from inside the wrapped function — the AutoGen agent
|
|
6
|
+
sees the raise the same as any other tool error.
|
|
7
|
+
|
|
8
|
+
This module is **experimental**. The legacy version exposed ``record_tool_result``,
|
|
9
|
+
``on_message``, and ``get_audit_report`` for telemetry; those depended on
|
|
10
|
+
internal interceptor methods that the public SDK does not expose. Use
|
|
11
|
+
``shieldops_sdk.telemetry`` for result/latency reporting instead.
|
|
12
|
+
|
|
13
|
+
Usage::
|
|
14
|
+
|
|
15
|
+
from shieldops_sdk.experimental.autogen import ShieldOpsAutoGenWrapper
|
|
16
|
+
|
|
17
|
+
wrapper = ShieldOpsAutoGenWrapper(api_key="sk-...", mode="enforce")
|
|
18
|
+
secured_agent = wrapper.wrap_agent(my_autogen_agent)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import functools
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from shieldops_sdk.config import SDKMode, ShieldOpsConfig
|
|
28
|
+
from shieldops_sdk.interceptor import ShieldOpsInterceptor
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("shieldops_sdk.experimental.autogen")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ShieldOpsAutoGenWrapper:
|
|
34
|
+
"""Wraps Microsoft AutoGen agents with ShieldOps firewall interception."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
api_key: str = "",
|
|
39
|
+
endpoint: str = "https://api.shieldops.io",
|
|
40
|
+
mode: str = "audit",
|
|
41
|
+
agent_id: str = "autogen-agent",
|
|
42
|
+
) -> None:
|
|
43
|
+
config = ShieldOpsConfig(
|
|
44
|
+
api_key=api_key,
|
|
45
|
+
endpoint=endpoint,
|
|
46
|
+
mode=SDKMode(mode),
|
|
47
|
+
)
|
|
48
|
+
self._config = config
|
|
49
|
+
self._interceptor = ShieldOpsInterceptor(config)
|
|
50
|
+
self._agent_id = agent_id
|
|
51
|
+
logger.info(
|
|
52
|
+
"shieldops.autogen.initialized agent_id=%s mode=%s",
|
|
53
|
+
agent_id,
|
|
54
|
+
mode,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def wrap_tool_execution(
|
|
58
|
+
self,
|
|
59
|
+
tool_name: str,
|
|
60
|
+
tool_args: dict[str, Any] | None = None,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
"""Evaluate a tool call before execution.
|
|
63
|
+
|
|
64
|
+
Returns ``{"action", "risk_score"}``. In ``enforce`` mode a denied call
|
|
65
|
+
raises ``ShieldOpsDeniedError`` instead of returning.
|
|
66
|
+
"""
|
|
67
|
+
decision = self._interceptor.check(
|
|
68
|
+
tool_name,
|
|
69
|
+
tool_args or {},
|
|
70
|
+
agent_id=self._agent_id,
|
|
71
|
+
)
|
|
72
|
+
return {"action": decision.action, "risk_score": decision.risk_score}
|
|
73
|
+
|
|
74
|
+
def wrap_agent(self, agent: Any) -> Any:
|
|
75
|
+
"""Monkey-patch ``agent.execute_function`` to route through the interceptor.
|
|
76
|
+
|
|
77
|
+
Returns the same agent for chainability. If the agent has no
|
|
78
|
+
``execute_function`` attribute, this is a no-op.
|
|
79
|
+
"""
|
|
80
|
+
if not hasattr(agent, "execute_function"):
|
|
81
|
+
logger.info(
|
|
82
|
+
"shieldops.autogen.wrap_agent_noop agent=%s reason=no_execute_function",
|
|
83
|
+
getattr(agent, "name", agent),
|
|
84
|
+
)
|
|
85
|
+
return agent
|
|
86
|
+
|
|
87
|
+
interceptor = self._interceptor
|
|
88
|
+
agent_id = self._agent_id
|
|
89
|
+
original_exec = agent.execute_function
|
|
90
|
+
|
|
91
|
+
@functools.wraps(original_exec)
|
|
92
|
+
def wrapped_exec(func_call: dict[str, Any], **kwargs: Any) -> Any:
|
|
93
|
+
tool_name = func_call.get("name", "unknown_function")
|
|
94
|
+
tool_args = func_call.get("arguments", {})
|
|
95
|
+
# check() raises ShieldOpsDeniedError in enforce mode for denied calls.
|
|
96
|
+
interceptor.check(tool_name, tool_args, agent_id=agent_id)
|
|
97
|
+
return original_exec(func_call, **kwargs)
|
|
98
|
+
|
|
99
|
+
agent.execute_function = wrapped_exec
|
|
100
|
+
logger.info(
|
|
101
|
+
"shieldops.autogen.agent_wrapped agent=%s",
|
|
102
|
+
getattr(agent, "name", agent),
|
|
103
|
+
)
|
|
104
|
+
return agent
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def interceptor(self) -> ShieldOpsInterceptor:
|
|
108
|
+
"""Underlying interceptor (for advanced telemetry/stats access)."""
|
|
109
|
+
return self._interceptor
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def stats(self) -> dict[str, Any]:
|
|
113
|
+
"""Interception statistics."""
|
|
114
|
+
return self._interceptor.stats
|