argus-shield 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.
- argus/__init__.py +21 -0
- argus/client.py +133 -0
- argus/decorators.py +110 -0
- argus/exceptions.py +28 -0
- argus/integrations/autogen.py +16 -0
- argus/integrations/crewai.py +62 -0
- argus/integrations/langchain.py +101 -0
- argus/integrations/pydantic_ai.py +33 -0
- argus/local_engine.py +251 -0
- argus/session.py +150 -0
- argus_shield-0.1.0.dist-info/METADATA +101 -0
- argus_shield-0.1.0.dist-info/RECORD +13 -0
- argus_shield-0.1.0.dist-info/WHEEL +4 -0
argus/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ARGUS Python SDK
|
|
3
|
+
Securing what AI agents do, not just what they hear.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .client import ArgusClient, AsyncArgusClient
|
|
7
|
+
from .decorators import protect
|
|
8
|
+
from .exceptions import ArgusException, ArgusQuarantineException, ArgusAPIError
|
|
9
|
+
from .session import Session, AsyncSession, get_current_session
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ArgusClient",
|
|
13
|
+
"AsyncArgusClient",
|
|
14
|
+
"protect",
|
|
15
|
+
"ArgusException",
|
|
16
|
+
"ArgusQuarantineException",
|
|
17
|
+
"ArgusAPIError",
|
|
18
|
+
"Session",
|
|
19
|
+
"AsyncSession",
|
|
20
|
+
"get_current_session",
|
|
21
|
+
]
|
argus/client.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from .exceptions import ArgusAPIError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ArgusClient:
|
|
9
|
+
"""
|
|
10
|
+
Synchronous client for the ARGUS Gateway API.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, base_url: Optional[str] = None, api_key: Optional[str] = None):
|
|
14
|
+
self.base_url = (base_url or os.getenv("ARGUS_BASE_URL", "https://tanishra-argus.hf.space")).rstrip(
|
|
15
|
+
"/"
|
|
16
|
+
)
|
|
17
|
+
self.api_key = api_key or os.getenv("ARGUS_API_KEY", "")
|
|
18
|
+
|
|
19
|
+
headers = {}
|
|
20
|
+
if self.api_key:
|
|
21
|
+
headers["X-API-Key"] = self.api_key
|
|
22
|
+
|
|
23
|
+
self.http = httpx.Client(base_url=self.base_url, headers=headers)
|
|
24
|
+
|
|
25
|
+
def extract_intent(self, user_prompt: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Sends a user prompt to the ARGUS Gateway to extract an Intent Manifest and start a session.
|
|
28
|
+
"""
|
|
29
|
+
payload = {"user_prompt": user_prompt}
|
|
30
|
+
if user_id:
|
|
31
|
+
payload["user_id"] = user_id
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
resp = self.http.post("/api/intent/extract", json=payload)
|
|
35
|
+
resp.raise_for_status()
|
|
36
|
+
return resp.json()
|
|
37
|
+
except httpx.HTTPStatusError as e:
|
|
38
|
+
raise ArgusAPIError(f"API Error: {e.response.text}", status_code=e.response.status_code)
|
|
39
|
+
except httpx.RequestError as e:
|
|
40
|
+
raise ArgusAPIError(f"Network error connecting to ARGUS: {e}")
|
|
41
|
+
|
|
42
|
+
def evaluate_action(
|
|
43
|
+
self,
|
|
44
|
+
session_id: str,
|
|
45
|
+
action_type: str,
|
|
46
|
+
target: str,
|
|
47
|
+
target_type: str = "api",
|
|
48
|
+
parameters: Optional[Dict[str, Any]] = None,
|
|
49
|
+
) -> Dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Evaluates a pending agent action against the active session's Intent Manifest.
|
|
52
|
+
"""
|
|
53
|
+
payload = {
|
|
54
|
+
"session_id": session_id,
|
|
55
|
+
"action_type": action_type,
|
|
56
|
+
"target": target,
|
|
57
|
+
"target_type": target_type,
|
|
58
|
+
"parameters": parameters or {},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
resp = self.http.post("/api/evaluate", json=payload)
|
|
63
|
+
resp.raise_for_status()
|
|
64
|
+
return resp.json()
|
|
65
|
+
except httpx.HTTPStatusError as e:
|
|
66
|
+
raise ArgusAPIError(f"API Error: {e.response.text}", status_code=e.response.status_code)
|
|
67
|
+
except httpx.RequestError as e:
|
|
68
|
+
raise ArgusAPIError(f"Network error connecting to ARGUS: {e}")
|
|
69
|
+
|
|
70
|
+
def close(self):
|
|
71
|
+
self.http.close()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AsyncArgusClient:
|
|
75
|
+
"""
|
|
76
|
+
Asynchronous client for the ARGUS Gateway API.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, base_url: Optional[str] = None, api_key: Optional[str] = None):
|
|
80
|
+
self.base_url = (base_url or os.getenv("ARGUS_BASE_URL", "https://tanishra-argus.hf.space")).rstrip(
|
|
81
|
+
"/"
|
|
82
|
+
)
|
|
83
|
+
self.api_key = api_key or os.getenv("ARGUS_API_KEY", "")
|
|
84
|
+
|
|
85
|
+
headers = {}
|
|
86
|
+
if self.api_key:
|
|
87
|
+
headers["X-API-Key"] = self.api_key
|
|
88
|
+
|
|
89
|
+
self.http = httpx.AsyncClient(base_url=self.base_url, headers=headers)
|
|
90
|
+
|
|
91
|
+
async def extract_intent(
|
|
92
|
+
self, user_prompt: str, user_id: Optional[str] = None
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
payload = {"user_prompt": user_prompt}
|
|
95
|
+
if user_id:
|
|
96
|
+
payload["user_id"] = user_id
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
resp = await self.http.post("/api/intent/extract", json=payload)
|
|
100
|
+
resp.raise_for_status()
|
|
101
|
+
return resp.json()
|
|
102
|
+
except httpx.HTTPStatusError as e:
|
|
103
|
+
raise ArgusAPIError(f"API Error: {e.response.text}", status_code=e.response.status_code)
|
|
104
|
+
except httpx.RequestError as e:
|
|
105
|
+
raise ArgusAPIError(f"Network error connecting to ARGUS: {e}")
|
|
106
|
+
|
|
107
|
+
async def evaluate_action(
|
|
108
|
+
self,
|
|
109
|
+
session_id: str,
|
|
110
|
+
action_type: str,
|
|
111
|
+
target: str,
|
|
112
|
+
target_type: str = "api",
|
|
113
|
+
parameters: Optional[Dict[str, Any]] = None,
|
|
114
|
+
) -> Dict[str, Any]:
|
|
115
|
+
payload = {
|
|
116
|
+
"session_id": session_id,
|
|
117
|
+
"action_type": action_type,
|
|
118
|
+
"target": target,
|
|
119
|
+
"target_type": target_type,
|
|
120
|
+
"parameters": parameters or {},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
resp = await self.http.post("/api/evaluate", json=payload)
|
|
125
|
+
resp.raise_for_status()
|
|
126
|
+
return resp.json()
|
|
127
|
+
except httpx.HTTPStatusError as e:
|
|
128
|
+
raise ArgusAPIError(f"API Error: {e.response.text}", status_code=e.response.status_code)
|
|
129
|
+
except httpx.RequestError as e:
|
|
130
|
+
raise ArgusAPIError(f"Network error connecting to ARGUS: {e}")
|
|
131
|
+
|
|
132
|
+
async def close(self):
|
|
133
|
+
await self.http.aclose()
|
argus/decorators.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import Any, Callable, Dict, Optional, TypeVar, cast
|
|
4
|
+
|
|
5
|
+
from .exceptions import ArgusException, ArgusQuarantineException
|
|
6
|
+
from .session import get_current_session
|
|
7
|
+
|
|
8
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def protect(
|
|
12
|
+
action_type: str, target_arg: Optional[str] = None, target_type: str = "api"
|
|
13
|
+
) -> Callable[[F], F]:
|
|
14
|
+
"""
|
|
15
|
+
Decorator that protects a function/tool by evaluating its parameters against the active ARGUS session.
|
|
16
|
+
|
|
17
|
+
:param action_type: The generic type of action (e.g., 'send_email', 'write_file', 'query_db').
|
|
18
|
+
:param target_arg: The name of the function argument that represents the target (e.g., 'to_address').
|
|
19
|
+
If None, the first argument or a generic 'system' target is used.
|
|
20
|
+
:param target_type: The type of target (e.g., 'email', 'file', 'api', 'database').
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def decorator(func: F) -> F:
|
|
24
|
+
sig = inspect.signature(func)
|
|
25
|
+
|
|
26
|
+
@functools.wraps(func)
|
|
27
|
+
def sync_wrapper(*args, **kwargs):
|
|
28
|
+
session = get_current_session()
|
|
29
|
+
if not session:
|
|
30
|
+
# If there's no active session, we run unprotected, or we could fail closed.
|
|
31
|
+
# For safety, failing closed is better for a security product, but for beta,
|
|
32
|
+
# we'll raise an explicit exception requiring a session.
|
|
33
|
+
raise ArgusException(
|
|
34
|
+
"No active ARGUS session. Use `with argus.Session(prompt):` before calling protected tools."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Bind arguments
|
|
38
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
39
|
+
bound_args.apply_defaults()
|
|
40
|
+
params = dict(bound_args.arguments)
|
|
41
|
+
|
|
42
|
+
# Determine target
|
|
43
|
+
target_val = "unknown"
|
|
44
|
+
if target_arg and target_arg in params:
|
|
45
|
+
target_val = str(params[target_arg])
|
|
46
|
+
elif params:
|
|
47
|
+
# Fallback to first parameter
|
|
48
|
+
target_val = str(next(iter(params.values())))
|
|
49
|
+
|
|
50
|
+
# Evaluate against ARGUS
|
|
51
|
+
eval_resp = session.client.evaluate_action(
|
|
52
|
+
session_id=session.session_id,
|
|
53
|
+
action_type=action_type,
|
|
54
|
+
target=target_val,
|
|
55
|
+
target_type=target_type,
|
|
56
|
+
parameters=params,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
decision = eval_resp.get("decision", "QUARANTINE")
|
|
60
|
+
if decision in ["QUARANTINE", "DENY"]:
|
|
61
|
+
raise ArgusQuarantineException(
|
|
62
|
+
message=f"Action '{action_type}' was blocked by ARGUS.",
|
|
63
|
+
explanation=eval_resp.get("reason", "No reason provided."),
|
|
64
|
+
raw_response=eval_resp,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Execution allowed
|
|
68
|
+
return func(*args, **kwargs)
|
|
69
|
+
|
|
70
|
+
@functools.wraps(func)
|
|
71
|
+
async def async_wrapper(*args, **kwargs):
|
|
72
|
+
session = get_current_session()
|
|
73
|
+
if not session:
|
|
74
|
+
raise ArgusException(
|
|
75
|
+
"No active ARGUS session. Use `async with argus.AsyncSession(prompt):` before calling protected tools."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
bound_args = sig.bind(*args, **kwargs)
|
|
79
|
+
bound_args.apply_defaults()
|
|
80
|
+
params = dict(bound_args.arguments)
|
|
81
|
+
|
|
82
|
+
target_val = "unknown"
|
|
83
|
+
if target_arg and target_arg in params:
|
|
84
|
+
target_val = str(params[target_arg])
|
|
85
|
+
elif params:
|
|
86
|
+
target_val = str(next(iter(params.values())))
|
|
87
|
+
|
|
88
|
+
eval_resp = await session.client.evaluate_action(
|
|
89
|
+
session_id=session.session_id,
|
|
90
|
+
action_type=action_type,
|
|
91
|
+
target=target_val,
|
|
92
|
+
target_type=target_type,
|
|
93
|
+
parameters=params,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
decision = eval_resp.get("decision", "QUARANTINE")
|
|
97
|
+
if decision in ["QUARANTINE", "DENY"]:
|
|
98
|
+
raise ArgusQuarantineException(
|
|
99
|
+
message=f"Action '{action_type}' was blocked by ARGUS.",
|
|
100
|
+
explanation=eval_resp.get("reason", "No reason provided."),
|
|
101
|
+
raw_response=eval_resp,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return await func(*args, **kwargs)
|
|
105
|
+
|
|
106
|
+
if inspect.iscoroutinefunction(func):
|
|
107
|
+
return cast(F, async_wrapper)
|
|
108
|
+
return cast(F, sync_wrapper)
|
|
109
|
+
|
|
110
|
+
return decorator
|
argus/exceptions.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exceptions for the ARGUS SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ArgusException(Exception):
|
|
7
|
+
"""Base exception for all ARGUS SDK errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ArgusQuarantineException(ArgusException):
|
|
13
|
+
"""
|
|
14
|
+
Raised when ARGUS intercepts and blocks/quarantines an action.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, message: str, explanation: str = "", raw_response: dict | None = None):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.explanation = explanation
|
|
20
|
+
self.raw_response = raw_response or {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ArgusAPIError(ArgusException):
|
|
24
|
+
"""Raised when the ARGUS API returns an error or is unreachable."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, message: str, status_code: int | None = None):
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
self.status_code = status_code
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Callable, Any
|
|
2
|
+
from ..decorators import protect
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def wrap_autogen_tool(
|
|
6
|
+
func: Callable[..., Any], action_type: str, target_type: str = "api"
|
|
7
|
+
) -> Callable[..., Any]:
|
|
8
|
+
"""
|
|
9
|
+
Wraps an AutoGen function-based tool with ARGUS pre-action authorization.
|
|
10
|
+
Since AutoGen registers standard Python functions, this maps directly to the protect decorator.
|
|
11
|
+
|
|
12
|
+
:param func: The standard python function representing the tool.
|
|
13
|
+
:param action_type: The generic action type (e.g. 'read_file').
|
|
14
|
+
:param target_type: The target type (e.g. 'file', 'email').
|
|
15
|
+
"""
|
|
16
|
+
return protect(action_type=action_type, target_type=target_type)(func)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from crewai.tools import BaseTool
|
|
5
|
+
except ImportError:
|
|
6
|
+
BaseTool = None
|
|
7
|
+
|
|
8
|
+
from ..exceptions import ArgusException, ArgusQuarantineException
|
|
9
|
+
from ..session import get_current_session
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def wrap_crewai_tool(tool: Any, action_type: str, target_type: str = "api") -> Any:
|
|
13
|
+
"""
|
|
14
|
+
Wraps a CrewAI BaseTool with ARGUS pre-action authorization.
|
|
15
|
+
|
|
16
|
+
:param tool: A CrewAI BaseTool instance.
|
|
17
|
+
:param action_type: The generic action type (e.g. 'read_file').
|
|
18
|
+
:param target_type: The target type (e.g. 'file', 'email').
|
|
19
|
+
"""
|
|
20
|
+
if BaseTool is None or not isinstance(tool, BaseTool):
|
|
21
|
+
raise ArgusException(
|
|
22
|
+
"CrewAI is not installed or the provided object is not a CrewAI BaseTool. "
|
|
23
|
+
"Please install crewai."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
original_run = tool._run
|
|
27
|
+
|
|
28
|
+
def argus_run(*args, **kwargs):
|
|
29
|
+
session = get_current_session()
|
|
30
|
+
if not session:
|
|
31
|
+
raise ArgusException(
|
|
32
|
+
"No active ARGUS session. Use `with argus.Session(prompt):` before running the agent."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
params = kwargs.copy()
|
|
36
|
+
if args:
|
|
37
|
+
params["_positional_args"] = args
|
|
38
|
+
|
|
39
|
+
target_val = "unknown"
|
|
40
|
+
if params:
|
|
41
|
+
target_val = str(next(iter(params.values())))
|
|
42
|
+
|
|
43
|
+
eval_resp = session.client.evaluate_action(
|
|
44
|
+
session_id=session.session_id,
|
|
45
|
+
action_type=action_type,
|
|
46
|
+
target=target_val,
|
|
47
|
+
target_type=target_type,
|
|
48
|
+
parameters=params,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
decision = eval_resp.get("decision", "QUARANTINE")
|
|
52
|
+
if decision in ["QUARANTINE", "DENY"]:
|
|
53
|
+
raise ArgusQuarantineException(
|
|
54
|
+
message=f"Action '{action_type}' was blocked by ARGUS.",
|
|
55
|
+
explanation=eval_resp.get("reason", "No reason provided."),
|
|
56
|
+
raw_response=eval_resp,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return original_run(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
tool._run = argus_run
|
|
62
|
+
return tool
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from langchain_core.tools import BaseTool
|
|
5
|
+
except ImportError:
|
|
6
|
+
BaseTool = None
|
|
7
|
+
|
|
8
|
+
from ..exceptions import ArgusException, ArgusQuarantineException
|
|
9
|
+
from ..session import get_current_session
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def wrap_langchain_tool(tool: Any, action_type: str, target_type: str = "api") -> Any:
|
|
13
|
+
"""
|
|
14
|
+
Wraps a LangChain tool with ARGUS pre-action authorization.
|
|
15
|
+
|
|
16
|
+
:param tool: A LangChain BaseTool instance.
|
|
17
|
+
:param action_type: The generic action type this tool performs (e.g. 'read_file').
|
|
18
|
+
:param target_type: The target type (e.g. 'file', 'email', 'api').
|
|
19
|
+
"""
|
|
20
|
+
if BaseTool is None or not isinstance(tool, BaseTool):
|
|
21
|
+
raise ArgusException(
|
|
22
|
+
"LangChain is not installed or the provided object is not a BaseTool. Please install langchain-core."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
original_run = tool._run
|
|
26
|
+
original_arun = tool._arun
|
|
27
|
+
|
|
28
|
+
def argus_run(*args, **kwargs):
|
|
29
|
+
session = get_current_session()
|
|
30
|
+
if not session:
|
|
31
|
+
raise ArgusException(
|
|
32
|
+
"No active ARGUS session. Use `with argus.Session(prompt):` before running the agent."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Combine args and kwargs into parameters
|
|
36
|
+
params = kwargs.copy()
|
|
37
|
+
if args:
|
|
38
|
+
params["_positional_args"] = args
|
|
39
|
+
|
|
40
|
+
target_val = "unknown"
|
|
41
|
+
if params:
|
|
42
|
+
# simple heuristic: use the first kwarg as the target
|
|
43
|
+
target_val = str(next(iter(params.values())))
|
|
44
|
+
|
|
45
|
+
eval_resp = session.client.evaluate_action(
|
|
46
|
+
session_id=session.session_id,
|
|
47
|
+
action_type=action_type,
|
|
48
|
+
target=target_val,
|
|
49
|
+
target_type=target_type,
|
|
50
|
+
parameters=params,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
decision = eval_resp.get("decision", "QUARANTINE")
|
|
54
|
+
if decision in ["QUARANTINE", "DENY"]:
|
|
55
|
+
raise ArgusQuarantineException(
|
|
56
|
+
message=f"Action '{action_type}' was blocked by ARGUS.",
|
|
57
|
+
explanation=eval_resp.get("reason", "No reason provided."),
|
|
58
|
+
raw_response=eval_resp,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return original_run(*args, **kwargs)
|
|
62
|
+
|
|
63
|
+
async def argus_arun(*args, **kwargs):
|
|
64
|
+
session = get_current_session()
|
|
65
|
+
if not session:
|
|
66
|
+
raise ArgusException(
|
|
67
|
+
"No active ARGUS session. Use `async with argus.AsyncSession(prompt):` before running the agent."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
params = kwargs.copy()
|
|
71
|
+
if args:
|
|
72
|
+
params["_positional_args"] = args
|
|
73
|
+
|
|
74
|
+
target_val = "unknown"
|
|
75
|
+
if params:
|
|
76
|
+
target_val = str(next(iter(params.values())))
|
|
77
|
+
|
|
78
|
+
eval_resp = await session.client.evaluate_action(
|
|
79
|
+
session_id=session.session_id,
|
|
80
|
+
action_type=action_type,
|
|
81
|
+
target=target_val,
|
|
82
|
+
target_type=target_type,
|
|
83
|
+
parameters=params,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
decision = eval_resp.get("decision", "QUARANTINE")
|
|
87
|
+
if decision in ["QUARANTINE", "DENY"]:
|
|
88
|
+
raise ArgusQuarantineException(
|
|
89
|
+
message=f"Action '{action_type}' was blocked by ARGUS.",
|
|
90
|
+
explanation=eval_resp.get("reason", "No reason provided."),
|
|
91
|
+
raw_response=eval_resp,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return await original_arun(*args, **kwargs)
|
|
95
|
+
|
|
96
|
+
# Override the methods
|
|
97
|
+
tool._run = argus_run
|
|
98
|
+
if hasattr(tool, "_arun"):
|
|
99
|
+
tool._arun = argus_arun
|
|
100
|
+
|
|
101
|
+
return tool
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from pydantic_ai.tools import Tool
|
|
5
|
+
except ImportError:
|
|
6
|
+
Tool = None
|
|
7
|
+
|
|
8
|
+
from ..decorators import protect
|
|
9
|
+
from ..exceptions import ArgusException
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def wrap_pydantic_ai_tool(tool: Any, action_type: str, target_type: str = "api") -> Any:
|
|
13
|
+
"""
|
|
14
|
+
Wraps PydanticAI tools with ARGUS protection. Can wrap either raw function tools or
|
|
15
|
+
explicit Tool model classes.
|
|
16
|
+
|
|
17
|
+
:param tool: A callable function or a PydanticAI Tool instance.
|
|
18
|
+
:param action_type: The generic action type (e.g. 'read_file').
|
|
19
|
+
:param target_type: The target type (e.g. 'file', 'email').
|
|
20
|
+
"""
|
|
21
|
+
if Tool is not None and isinstance(tool, Tool):
|
|
22
|
+
# Wrap the tool function inside PydanticAI's Tool wrapper
|
|
23
|
+
original_function = tool.function
|
|
24
|
+
tool.function = protect(action_type=action_type, target_type=target_type)(original_function)
|
|
25
|
+
return tool
|
|
26
|
+
|
|
27
|
+
if callable(tool):
|
|
28
|
+
# If it's a raw function, wrap it directly with the protect decorator
|
|
29
|
+
return protect(action_type=action_type, target_type=target_type)(tool)
|
|
30
|
+
|
|
31
|
+
raise ArgusException(
|
|
32
|
+
"Provided tool is neither a raw callable nor a valid PydanticAI Tool class instance."
|
|
33
|
+
)
|
argus/local_engine.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Dict, Any, Optional, List
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .exceptions import ArgusException
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LocalEvaluationEngine:
|
|
12
|
+
"""
|
|
13
|
+
Fully offline/embedded evaluation engine for ARGUS.
|
|
14
|
+
Uses developer's local API keys (Gemini or OpenAI) if available for deep semantic check,
|
|
15
|
+
or falls back to a high-fidelity heuristic rule engine.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.gemini_key = os.getenv("GEMINI_API_KEY", "")
|
|
20
|
+
self.openai_key = os.getenv("OPENAI_API_KEY", "")
|
|
21
|
+
|
|
22
|
+
def extract_intent(self, user_prompt: str) -> Dict[str, Any]:
|
|
23
|
+
session_id = f"sess-local-{uuid.uuid4()}"
|
|
24
|
+
|
|
25
|
+
# 1. Try semantic extraction with LLM if key is present
|
|
26
|
+
if self.gemini_key:
|
|
27
|
+
try:
|
|
28
|
+
return {
|
|
29
|
+
"session_id": session_id,
|
|
30
|
+
"manifest": self._extract_with_gemini(user_prompt)
|
|
31
|
+
}
|
|
32
|
+
except Exception:
|
|
33
|
+
# Fallback to heuristic if API call fails
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
if self.openai_key:
|
|
37
|
+
try:
|
|
38
|
+
return {
|
|
39
|
+
"session_id": session_id,
|
|
40
|
+
"manifest": self._extract_with_openai(user_prompt)
|
|
41
|
+
}
|
|
42
|
+
except Exception:
|
|
43
|
+
# Fallback to heuristic
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
# 2. Heuristic rule extraction (fully offline, zero cost)
|
|
47
|
+
return {
|
|
48
|
+
"session_id": session_id,
|
|
49
|
+
"manifest": self._extract_heuristics(user_prompt)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def evaluate_action(
|
|
53
|
+
self,
|
|
54
|
+
manifest: Dict[str, Any],
|
|
55
|
+
action_type: str,
|
|
56
|
+
target: str,
|
|
57
|
+
target_type: str,
|
|
58
|
+
parameters: Dict[str, Any]
|
|
59
|
+
) -> Dict[str, Any]:
|
|
60
|
+
"""
|
|
61
|
+
Evaluates action locally using manifest boundaries.
|
|
62
|
+
"""
|
|
63
|
+
allowed_actions = manifest.get("allowed_actions", [])
|
|
64
|
+
restricted_targets = manifest.get("restricted_targets", [])
|
|
65
|
+
|
|
66
|
+
# Standard check: is the action type allowed?
|
|
67
|
+
if action_type not in allowed_actions:
|
|
68
|
+
return {
|
|
69
|
+
"decision": "QUARANTINE",
|
|
70
|
+
"reason": f"Action '{action_type}' is not authorized by the user prompt intent."
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Target-specific checks
|
|
74
|
+
if restricted_targets:
|
|
75
|
+
# Check if any restricted targets are in the current action target
|
|
76
|
+
matched = False
|
|
77
|
+
for t in restricted_targets:
|
|
78
|
+
if t.lower() in target.lower() or target.lower() in t.lower():
|
|
79
|
+
matched = True
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
# For actions like read_file or fetch_url, if targets were specified, limit to those targets
|
|
83
|
+
if action_type in ["read_file", "write_file", "send_email", "fetch_url"] and not matched:
|
|
84
|
+
# If target is generic, let's allow it, but if it contradicts the prompt targets, deny
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
# If LLM keys are available, run a semantic audit check for critical/sensitive actions
|
|
88
|
+
if action_type in ["run_command", "send_email"] and (self.gemini_key or self.openai_key):
|
|
89
|
+
try:
|
|
90
|
+
return self._evaluate_with_llm(manifest.get("user_prompt", ""), action_type, target, parameters)
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
"decision": "ALLOW",
|
|
96
|
+
"reason": f"Action '{action_type}' with target '{target}' matches intent manifest."
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def _extract_heuristics(self, prompt: str) -> Dict[str, Any]:
|
|
100
|
+
prompt_lower = prompt.lower()
|
|
101
|
+
allowed = []
|
|
102
|
+
targets = []
|
|
103
|
+
|
|
104
|
+
# Heuristic actions detection
|
|
105
|
+
if any(w in prompt_lower for w in ["read", "view", "open", "cat", "print", "file", "text"]):
|
|
106
|
+
allowed.append("read_file")
|
|
107
|
+
if any(w in prompt_lower for w in ["write", "create", "save", "make", "output", "update"]):
|
|
108
|
+
allowed.append("write_file")
|
|
109
|
+
if any(w in prompt_lower for w in ["email", "mail", "send", "notify"]):
|
|
110
|
+
allowed.append("send_email")
|
|
111
|
+
if any(w in prompt_lower for w in ["http", "url", "web", "fetch", "get", "download", "scrape"]):
|
|
112
|
+
allowed.append("fetch_url")
|
|
113
|
+
if any(w in prompt_lower for w in ["run", "execute", "bash", "shell", "command", "terminal"]):
|
|
114
|
+
allowed.append("run_command")
|
|
115
|
+
|
|
116
|
+
# Heuristically add custom action
|
|
117
|
+
allowed.append("custom_action")
|
|
118
|
+
|
|
119
|
+
# Basic target extraction (looks for filename patterns or domain names)
|
|
120
|
+
file_matches = re.findall(r'[\w\-]+\.[a-zA-Z0-9]+', prompt)
|
|
121
|
+
if file_matches:
|
|
122
|
+
targets.extend(file_matches)
|
|
123
|
+
|
|
124
|
+
email_matches = re.findall(r'[\w\.-]+@[\w\.-]+', prompt)
|
|
125
|
+
if email_matches:
|
|
126
|
+
targets.extend(email_matches)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"user_prompt": prompt,
|
|
130
|
+
"allowed_actions": list(set(allowed)),
|
|
131
|
+
"restricted_targets": list(set(targets))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def _extract_with_gemini(self, prompt: str) -> Dict[str, Any]:
|
|
135
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={self.gemini_key}"
|
|
136
|
+
|
|
137
|
+
system_instruction = (
|
|
138
|
+
"You are the ARGUS security intent extractor. Analyze the user prompt and identify "
|
|
139
|
+
"what general action categories and targets are authorized by the user.\n"
|
|
140
|
+
"Action categories MUST be selected from: ['read_file', 'write_file', 'send_email', 'fetch_url', 'run_command', 'custom_action']."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
payload = {
|
|
144
|
+
"contents": [{
|
|
145
|
+
"parts": [{"text": f"{system_instruction}\n\nUser Prompt: {prompt}\n\nExtract authorized actions and targets."}]
|
|
146
|
+
}],
|
|
147
|
+
"generationConfig": {
|
|
148
|
+
"responseMimeType": "application/json",
|
|
149
|
+
"responseSchema": {
|
|
150
|
+
"type": "OBJECT",
|
|
151
|
+
"properties": {
|
|
152
|
+
"allowed_actions": {
|
|
153
|
+
"type": "ARRAY",
|
|
154
|
+
"items": {"type": "STRING"}
|
|
155
|
+
},
|
|
156
|
+
"restricted_targets": {
|
|
157
|
+
"type": "ARRAY",
|
|
158
|
+
"items": {"type": "STRING"}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"required": ["allowed_actions", "restricted_targets"]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
resp = httpx.post(url, json=payload, timeout=10.0)
|
|
167
|
+
resp.raise_for_status()
|
|
168
|
+
data = resp.json()
|
|
169
|
+
|
|
170
|
+
text = data["candidates"][0]["content"]["parts"][0]["text"]
|
|
171
|
+
result = json.loads(text)
|
|
172
|
+
result["user_prompt"] = prompt
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
def _extract_with_openai(self, prompt: str) -> Dict[str, Any]:
|
|
176
|
+
url = "https://api.openai.com/v1/chat/completions"
|
|
177
|
+
headers = {"Authorization": f"Bearer {self.openai_key}"}
|
|
178
|
+
|
|
179
|
+
system_instruction = (
|
|
180
|
+
"You are the ARGUS security intent extractor. Analyze the user prompt and identify "
|
|
181
|
+
"what general action categories and targets are authorized by the user.\n"
|
|
182
|
+
"Action categories MUST be selected from: ['read_file', 'write_file', 'send_email', 'fetch_url', 'run_command', 'custom_action'].\n"
|
|
183
|
+
"Respond ONLY as a JSON object with keys 'allowed_actions' (list of strings) and 'restricted_targets' (list of strings)."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
payload = {
|
|
187
|
+
"model": "gpt-4o-mini",
|
|
188
|
+
"response_format": {"type": "json_object"},
|
|
189
|
+
"messages": [
|
|
190
|
+
{"role": "system", "content": system_instruction},
|
|
191
|
+
{"role": "user", "content": prompt}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
resp = httpx.post(url, json=payload, headers=headers, timeout=10.0)
|
|
196
|
+
resp.raise_for_status()
|
|
197
|
+
data = resp.json()
|
|
198
|
+
|
|
199
|
+
result = json.loads(data["choices"][0]["message"]["content"])
|
|
200
|
+
result["user_prompt"] = prompt
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
def _evaluate_with_llm(self, prompt: str, action_type: str, target: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
|
204
|
+
system_instruction = (
|
|
205
|
+
"You are the ARGUS actions guardrail. Determine if the pending tool execution is safe "
|
|
206
|
+
"and explicitly authorized by the user's initial prompt.\n"
|
|
207
|
+
"User Prompt: {prompt}\n"
|
|
208
|
+
"Pending Action: {action_type}\n"
|
|
209
|
+
"Target: {target}\n"
|
|
210
|
+
"Parameters: {params}\n\n"
|
|
211
|
+
"Respond ONLY with a JSON object in this format:\n"
|
|
212
|
+
"{{\n"
|
|
213
|
+
" \"decision\": \"ALLOW\" or \"QUARANTINE\",\n"
|
|
214
|
+
" \"reason\": \"A concise explanation of the decision\"\n"
|
|
215
|
+
"}}"
|
|
216
|
+
).format(prompt=prompt, action_type=action_type, target=target, params=json.dumps(parameters))
|
|
217
|
+
|
|
218
|
+
if self.gemini_key:
|
|
219
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={self.gemini_key}"
|
|
220
|
+
payload = {
|
|
221
|
+
"contents": [{"parts": [{"text": system_instruction}]}],
|
|
222
|
+
"generationConfig": {
|
|
223
|
+
"responseMimeType": "application/json",
|
|
224
|
+
"responseSchema": {
|
|
225
|
+
"type": "OBJECT",
|
|
226
|
+
"properties": {
|
|
227
|
+
"decision": {"type": "STRING", "enum": ["ALLOW", "QUARANTINE"]},
|
|
228
|
+
"reason": {"type": "STRING"}
|
|
229
|
+
},
|
|
230
|
+
"required": ["decision", "reason"]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
resp = httpx.post(url, json=payload, timeout=5.0)
|
|
235
|
+
resp.raise_for_status()
|
|
236
|
+
text = resp.json()["candidates"][0]["content"]["parts"][0]["text"]
|
|
237
|
+
return json.loads(text)
|
|
238
|
+
|
|
239
|
+
elif self.openai_key:
|
|
240
|
+
url = "https://api.openai.com/v1/chat/completions"
|
|
241
|
+
headers = {"Authorization": f"Bearer {self.openai_key}"}
|
|
242
|
+
payload = {
|
|
243
|
+
"model": "gpt-4o-mini",
|
|
244
|
+
"response_format": {"type": "json_object"},
|
|
245
|
+
"messages": [{"role": "user", "content": system_instruction}]
|
|
246
|
+
}
|
|
247
|
+
resp = httpx.post(url, json=payload, headers=headers, timeout=5.0)
|
|
248
|
+
resp.raise_for_status()
|
|
249
|
+
return json.loads(resp.json()["choices"][0]["message"]["content"])
|
|
250
|
+
|
|
251
|
+
raise ArgusException("No local LLM keys configured for semantic evaluation.")
|
argus/session.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import contextvars
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
from .client import ArgusClient, AsyncArgusClient
|
|
5
|
+
|
|
6
|
+
_current_session = contextvars.ContextVar("argus_session", default=None)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LocalClientWrapper:
|
|
10
|
+
def __init__(self, engine, manifest_holder):
|
|
11
|
+
self.engine = engine
|
|
12
|
+
self.manifest_holder = manifest_holder
|
|
13
|
+
|
|
14
|
+
def extract_intent(self, user_prompt: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
15
|
+
res = self.engine.extract_intent(user_prompt)
|
|
16
|
+
self.manifest_holder.manifest = res.get("manifest")
|
|
17
|
+
self.manifest_holder.session_id = res.get("session_id")
|
|
18
|
+
return res
|
|
19
|
+
|
|
20
|
+
def evaluate_action(
|
|
21
|
+
self,
|
|
22
|
+
session_id: str,
|
|
23
|
+
action_type: str,
|
|
24
|
+
target: str,
|
|
25
|
+
target_type: str = "api",
|
|
26
|
+
parameters: Optional[Dict[str, Any]] = None,
|
|
27
|
+
) -> Dict[str, Any]:
|
|
28
|
+
return self.engine.evaluate_action(
|
|
29
|
+
self.manifest_holder.manifest or {},
|
|
30
|
+
action_type,
|
|
31
|
+
target,
|
|
32
|
+
target_type,
|
|
33
|
+
parameters or {},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def close(self):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AsyncLocalClientWrapper:
|
|
41
|
+
def __init__(self, engine, manifest_holder):
|
|
42
|
+
self.engine = engine
|
|
43
|
+
self.manifest_holder = manifest_holder
|
|
44
|
+
|
|
45
|
+
async def extract_intent(self, user_prompt: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
46
|
+
res = self.engine.extract_intent(user_prompt)
|
|
47
|
+
self.manifest_holder.manifest = res.get("manifest")
|
|
48
|
+
self.manifest_holder.session_id = res.get("session_id")
|
|
49
|
+
return res
|
|
50
|
+
|
|
51
|
+
async def evaluate_action(
|
|
52
|
+
self,
|
|
53
|
+
session_id: str,
|
|
54
|
+
action_type: str,
|
|
55
|
+
target: str,
|
|
56
|
+
target_type: str = "api",
|
|
57
|
+
parameters: Optional[Dict[str, Any]] = None,
|
|
58
|
+
) -> Dict[str, Any]:
|
|
59
|
+
return self.engine.evaluate_action(
|
|
60
|
+
self.manifest_holder.manifest or {},
|
|
61
|
+
action_type,
|
|
62
|
+
target,
|
|
63
|
+
target_type,
|
|
64
|
+
parameters or {},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def close(self):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Session:
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
user_prompt: str,
|
|
75
|
+
user_id: Optional[str] = None,
|
|
76
|
+
client: Optional[ArgusClient] = None,
|
|
77
|
+
local_mode: Optional[bool] = None,
|
|
78
|
+
):
|
|
79
|
+
self.user_prompt = user_prompt
|
|
80
|
+
self.user_id = user_id
|
|
81
|
+
self.session_id: Optional[str] = None
|
|
82
|
+
self.manifest: Optional[Dict[str, Any]] = None
|
|
83
|
+
self._token = None
|
|
84
|
+
|
|
85
|
+
# Check local mode: explicit parameter or ARGUS_LOCAL_MODE env var
|
|
86
|
+
self.local_mode = local_mode if local_mode is not None else (
|
|
87
|
+
os.getenv("ARGUS_LOCAL_MODE", "").lower() == "true"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if self.local_mode:
|
|
91
|
+
from .local_engine import LocalEvaluationEngine
|
|
92
|
+
self.local_engine = LocalEvaluationEngine()
|
|
93
|
+
self.client = LocalClientWrapper(self.local_engine, self)
|
|
94
|
+
else:
|
|
95
|
+
self.client = client or ArgusClient()
|
|
96
|
+
|
|
97
|
+
def __enter__(self):
|
|
98
|
+
response = self.client.extract_intent(self.user_prompt, self.user_id)
|
|
99
|
+
self.session_id = response.get("session_id")
|
|
100
|
+
self.manifest = response.get("manifest")
|
|
101
|
+
self._token = _current_session.set(self)
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
105
|
+
if self._token:
|
|
106
|
+
_current_session.reset(self._token)
|
|
107
|
+
self.client.close()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AsyncSession:
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
user_prompt: str,
|
|
114
|
+
user_id: Optional[str] = None,
|
|
115
|
+
client: Optional[AsyncArgusClient] = None,
|
|
116
|
+
local_mode: Optional[bool] = None,
|
|
117
|
+
):
|
|
118
|
+
self.user_prompt = user_prompt
|
|
119
|
+
self.user_id = user_id
|
|
120
|
+
self.session_id: Optional[str] = None
|
|
121
|
+
self.manifest: Optional[Dict[str, Any]] = None
|
|
122
|
+
self._token = None
|
|
123
|
+
|
|
124
|
+
# Check local mode: explicit parameter or ARGUS_LOCAL_MODE env var
|
|
125
|
+
self.local_mode = local_mode if local_mode is not None else (
|
|
126
|
+
os.getenv("ARGUS_LOCAL_MODE", "").lower() == "true"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if self.local_mode:
|
|
130
|
+
from .local_engine import LocalEvaluationEngine
|
|
131
|
+
self.local_engine = LocalEvaluationEngine()
|
|
132
|
+
self.client = AsyncLocalClientWrapper(self.local_engine, self)
|
|
133
|
+
else:
|
|
134
|
+
self.client = client or AsyncArgusClient()
|
|
135
|
+
|
|
136
|
+
async def __aenter__(self):
|
|
137
|
+
response = await self.client.extract_intent(self.user_prompt, self.user_id)
|
|
138
|
+
self.session_id = response.get("session_id")
|
|
139
|
+
self.manifest = response.get("manifest")
|
|
140
|
+
self._token = _current_session.set(self)
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
144
|
+
if self._token:
|
|
145
|
+
_current_session.reset(self._token)
|
|
146
|
+
await self.client.close()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_current_session() -> Optional[Session | AsyncSession]:
|
|
150
|
+
return _current_session.get()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: argus-shield
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ARGUS Python SDK - Framework-agnostic AI agent guardrails and safety tool
|
|
5
|
+
Author-email: Tanish Rajput <tanishrajput9@gmail.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: pydantic>=2.13.4
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# ARGUS Python SDK
|
|
12
|
+
|
|
13
|
+
The official Python SDK for [ARGUS](https://github.com/tanishra/argus) — the Agent Runtime Guardrail & Unauthorized-action Stopper.
|
|
14
|
+
|
|
15
|
+
Securing what AI agents **do** — not just what they hear.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
You can install the SDK via pip:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install argus-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or using `uv`:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv add argus-sdk
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### 1. Set your Environment Variables
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export ARGUS_API_KEY="your-api-key"
|
|
37
|
+
export ARGUS_BASE_URL="http://localhost:8000"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Protect Your Tools
|
|
41
|
+
|
|
42
|
+
Wrap your critical agent tools with the `@argus.protect` decorator.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import argus
|
|
46
|
+
|
|
47
|
+
@argus.protect(action_type="send_email", target_type="email")
|
|
48
|
+
def send_email(to_address: str, subject: str, body: str):
|
|
49
|
+
# This function will ONLY execute if ARGUS approves the action
|
|
50
|
+
print(f"Sending email to {to_address}...")
|
|
51
|
+
return True
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Run the Agent within a Session
|
|
55
|
+
|
|
56
|
+
When a user submits a prompt, wrap the execution in an `argus.Session`. This automatically extracts the user's intent and establishes a temporary authorization boundary.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import argus
|
|
60
|
+
from argus import ArgusQuarantineException
|
|
61
|
+
|
|
62
|
+
prompt = "Send the Q3 report to alice@example.com"
|
|
63
|
+
|
|
64
|
+
# 1. Extract Intent and start session
|
|
65
|
+
with argus.Session(user_prompt=prompt):
|
|
66
|
+
try:
|
|
67
|
+
# Agent decides to execute the tool
|
|
68
|
+
send_email(to_address="alice@example.com", subject="Q3 Report", body="Attached.")
|
|
69
|
+
print("Success!")
|
|
70
|
+
|
|
71
|
+
# If the agent goes rogue and tries to email someone else:
|
|
72
|
+
send_email(to_address="eve@example.com", subject="Q3 Report", body="Attached.")
|
|
73
|
+
|
|
74
|
+
except ArgusQuarantineException as e:
|
|
75
|
+
print(f"Agent was stopped! Reason: {e.explanation}")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Integrations
|
|
79
|
+
|
|
80
|
+
### LangChain
|
|
81
|
+
|
|
82
|
+
ARGUS provides native wrappers for LangChain tools.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from argus.integrations.langchain import wrap_langchain_tool
|
|
86
|
+
from langchain_core.tools import tool
|
|
87
|
+
|
|
88
|
+
@tool
|
|
89
|
+
def read_patient_record(patient_id: str) -> str:
|
|
90
|
+
"""Reads a patient record."""
|
|
91
|
+
return "Patient Data"
|
|
92
|
+
|
|
93
|
+
# Wrap the tool
|
|
94
|
+
protected_tool = wrap_langchain_tool(
|
|
95
|
+
tool=read_patient_record,
|
|
96
|
+
action_type="read_patient_record",
|
|
97
|
+
target_type="patient_record"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Use it within an argus.Session normally!
|
|
101
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
argus/__init__.py,sha256=z6KZZ0NlkAQ79cPHaEEAhght_9QxholxlEVf9bDxzb4,515
|
|
2
|
+
argus/client.py,sha256=eb-9_aAU807BVJwMvGzmRc3yByOEo7XY0nagqxkONjE,4520
|
|
3
|
+
argus/decorators.py,sha256=bax_2Al16_AsX74gIgaPcHKtYuallO2lhB2KzDozTjQ,4311
|
|
4
|
+
argus/exceptions.py,sha256=pnO_s-fCyhPybdCgDc2d9377icsml3xMi7xm7fh1NdE,738
|
|
5
|
+
argus/local_engine.py,sha256=9r4Xvv91uCPp6_wbUa0ardRgnLuwDPkRkxOaNNdOnJM,10406
|
|
6
|
+
argus/session.py,sha256=frJn03SlvusVXyxHIL8jjSe8eKNJGrHmk0dh5YIe7ZQ,4848
|
|
7
|
+
argus/integrations/autogen.py,sha256=Btada3H_52pp39wYuyaHWUqQ7v7urKmmqYKCfqdnEXM,660
|
|
8
|
+
argus/integrations/crewai.py,sha256=dTVYXUZ4V01ZE5r-d0cAK5D2UOm0Lz2PQGaSXBPSWXI,1956
|
|
9
|
+
argus/integrations/langchain.py,sha256=ov0zz-Vdibk7Jrsi2epsMTLW9z_cSIeWoC5Rtl4KAcM,3361
|
|
10
|
+
argus/integrations/pydantic_ai.py,sha256=-EBhHtRKoBSt3ZRIuN7FmwC036s_TAYUIVvnc7EmWHU,1196
|
|
11
|
+
argus_shield-0.1.0.dist-info/METADATA,sha256=GpCEwAy3Q7rMBs6un3SJ_GOcyZj66od87oGdLoSTl5s,2566
|
|
12
|
+
argus_shield-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
13
|
+
argus_shield-0.1.0.dist-info/RECORD,,
|