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 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any