Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5b3a079
feat(rbac): Implement Role-Based Access Control (RBAC) system
hassan11196 Feb 4, 2026
ce9a6dc
Merge remote-tracking branch 'upstream/main' into role-based-sso-auth
hassan11196 Feb 4, 2026
0c55e53
Merge remote-tracking branch 'upstream/main' into role-based-sso-auth
hassan11196 Feb 4, 2026
b691c06
feat(http-get): Add HTTP GET tool for fetching live data from URLs wi…
hassan11196 Feb 4, 2026
192d22b
fix: Update login template to redirect to landing page instead of log…
hassan11196 Feb 5, 2026
fb0ebb9
feat(rbac): Enhance role context handling and description retrieval f…
hassan11196 Feb 5, 2026
28f20ca
fix: Update login template reference in Flask app for consistency
hassan11196 Feb 5, 2026
77c775e
refactor: Remove debug_token_info endpoint and associated logic from …
hassan11196 Feb 5, 2026
97de0ee
feat(rbac): Improve RBAC validation and error handling in registry
hassan11196 Feb 5, 2026
e43a07d
Merge remote-tracking branch 'upstream/main' into role-based-sso-auth
hassan11196 Feb 5, 2026
033b5ab
feat: add sandbox execution tool for isolated code execution in Docker
hassan11196 Feb 5, 2026
f80aa88
feat: implement sandbox execution tool with artifact persistence and …
hassan11196 Feb 6, 2026
0ed6a88
feat: add sandbox artifact handling and serving endpoints
hassan11196 Feb 6, 2026
703a534
feat: Implement sandbox approval mechanism for code execution
hassan11196 Feb 6, 2026
a63dc8f
Merge remote-tracking branch 'upstream/main' into feat-containerized-…
hassan11196 Feb 20, 2026
e5ce63f
bugfix: fix chatjs bug after merge
hassan11196 Feb 20, 2026
c84e75e
feat: add sandbox tools to the agent's tools module
hassan11196 Feb 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ dependencies = [
"pandas==2.3.2",
"isort==6.0.1",
"pre-commit>=4",
"psycopg2-binary==2.9.10"
"psycopg2-binary==2.9.10",
"docker==7.1.0"
]

[project.scripts]
Expand Down
1 change: 1 addition & 0 deletions requirements/requirements-base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ aiohttp==3.9.5
nltk==3.9.1
sentence-transformers==5.1.2
rank_bm25==0.2.2
docker==7.1.0
46 changes: 45 additions & 1 deletion src/archi/pipelines/agents/base_react.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,38 @@
logger = get_logger(__name__)


def _get_role_context() -> str:
"""
Get role context string for the current user if enabled.

Requires SSO auth with auth_roles configured and pass_descriptions_to_agent: true.
Returns empty string if conditions not met or user not authenticated.
"""
try:
from flask import session, has_request_context
if not has_request_context():
return ""
if not session.get('logged_in'):
return ""

from src.utils.rbac.registry import get_registry
registry = get_registry()

if not registry.pass_descriptions_to_agent:
return ""

roles = session.get('roles', [])
if not roles:
return ""

descriptions = registry.get_role_descriptions(roles)
if descriptions:
return f"\n\nUser roles: {descriptions}."
return ""
except Exception as e:
logger.debug(f"Could not get role context: {e}")
return ""

class BaseReActAgent:
"""
BaseReActAgent provides a foundational structure for building pipeline classes that
Expand Down Expand Up @@ -835,16 +867,28 @@ def refresh_agent(
self._active_middleware = list(middleware)
return self.agent

def _build_system_prompt(self) -> str:
"""
Build the full system prompt, appending role context if enabled.

Role context is appended when SSO auth with auth_roles is configured
and pass_descriptions_to_agent is set to true.
"""
base_prompt = self.agent_prompt or ""
role_context = _get_role_context()
return base_prompt + role_context

def _create_agent(self, tools: Sequence[Callable], middleware: Sequence[Callable]) -> CompiledStateGraph:
"""Create the LangGraph agent with the specified LLM, tools, and system prompt."""
system_prompt = self._build_system_prompt()
logger.debug("Creating agent %s with:", self.__class__.__name__)
logger.debug("%d tools", len(tools))
logger.debug("%d middleware components", len(middleware))
return create_agent(
model=self.agent_llm,
tools=tools,
middleware=middleware,
system_prompt=self.agent_prompt,
system_prompt=system_prompt,
)

def _build_static_tools(self) -> List[Callable]:
Expand Down
34 changes: 34 additions & 0 deletions src/archi/pipelines/agents/cms_comp_ops_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
create_metadata_search_tool,
create_metadata_schema_tool,
create_retriever_tool,
create_http_get_tool,
create_sandbox_tool,
initialize_mcp_client,
RemoteCatalogClient,
MONITOpenSearchClient,
create_monit_opensearch_search_tool,
Expand Down Expand Up @@ -170,6 +173,37 @@ def _build_fetch_tool(self) -> Callable:
description=description,
)

http_get_tool = create_http_get_tool(
name="fetch_url",
description=(
"Fetch live content from a URL via HTTP GET request. "
"Input: A valid HTTP or HTTPS URL. "
"Output: The response body text or an error message. "
"Use this to retrieve real-time data from web endpoints, APIs, documentation, or status pages. "
"Examples: checking endpoint status, fetching API data, retrieving documentation."
),
timeout=15.0,
max_response_chars=600000,
)

all_tools = [file_search_tool, metadata_search_tool, metadata_schema_tool, fetch_tool, http_get_tool]

# Add sandbox tool for code execution in isolated containers
sandbox_tool = create_sandbox_tool(
name="run_code",
description=(
"Execute code in a secure sandboxed Docker container. "
"Input: code (str), language ('python', 'bash', or 'sh'). "
"Output: stdout, stderr, exit code, and any files written to /workspace/output/. "
"The /workspace/ and /workspace/output/ directories are pre-created and writable — "
"do NOT call os.makedirs() for them. Do not show internal sandbox paths in the output. "
"Use this for running Python scripts, shell commands, data processing, API calls with curl, "
"rucio commands, or any code that needs to be executed safely. "
"The container is ephemeral and destroyed after execution."
),
)
all_tools.append(sandbox_tool)
logger.info("Sandbox tool added to CMSCompOpsAgent")
def _build_vector_tool_placeholder(self) -> List[Callable]:
return []

Expand Down
17 changes: 17 additions & 0 deletions src/archi/pipelines/agents/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .base import check_tool_permission, require_tool_permission
from .local_files import (
create_document_fetch_tool,
create_file_search_tool,
Expand All @@ -12,15 +13,31 @@
create_monit_opensearch_search_tool,
create_monit_opensearch_aggregation_tool,
)
from .http_get import create_http_get_tool
from .sandbox import (
create_sandbox_tool,
create_sandbox_tool_with_files,
set_sandbox_context,
get_sandbox_artifacts,
clear_sandbox_context,
)

__all__ = [
"check_tool_permission",
"require_tool_permission",
"create_document_fetch_tool",
"create_file_search_tool",
"create_metadata_search_tool",
"create_metadata_schema_tool",
"RemoteCatalogClient",
"create_retriever_tool",
"initialize_mcp_client",
"create_http_get_tool",
"create_sandbox_tool",
"create_sandbox_tool_with_files",
"set_sandbox_context",
"get_sandbox_artifacts",
"clear_sandbox_context",
"MONITOpenSearchClient",
"create_monit_opensearch_search_tool",
"create_monit_opensearch_aggregation_tool",
Expand Down
123 changes: 121 additions & 2 deletions src/archi/pipelines/agents/tools/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,127 @@
"""Abstract base class for all tools."""
"""Base utilities and RBAC decorators for agent tools."""

from __future__ import annotations

from functools import wraps
from typing import Callable, Optional, TypeVar

from typing import Callable
from langchain.tools import tool

from src.utils.logging import get_logger

logger = get_logger(__name__)


# Type variable for generic function signatures
F = TypeVar('F', bound=Callable)


def check_tool_permission(required_permission: str) -> tuple[bool, Optional[str]]:
"""
Check if the current user has permission to use a tool.

Uses the Flask session to get user roles and checks against the RBAC registry.
This function is designed to fail open in non-web contexts (CLI, testing)
and when the RBAC system is not configured.

Args:
required_permission: The permission string to check (e.g., 'tools:http_get')

Returns:
(has_permission, error_message) tuple where:
- has_permission is True if access is granted
- error_message is None if granted, or a user-friendly error string if denied
"""
try:
from flask import session, has_request_context
from src.utils.rbac.registry import get_registry

# If we're not in a request context, allow the tool (for testing/CLI usage)
if not has_request_context():
logger.debug("No request context, allowing tool access")
return True, None

# Get user roles from session
if not session.get('logged_in'):
logger.warning("User not logged in, denying tool access")
return False, "You must be logged in to use this feature."

user_roles = session.get('roles', [])

# Check permission using RBAC registry
try:
registry = get_registry()
if registry.has_permission(user_roles, required_permission):
logger.debug(f"User with roles {user_roles} granted permission '{required_permission}'")
return True, None
else:
logger.info(f"User with roles {user_roles} denied permission '{required_permission}'")
return False, (
f"Permission denied: This tool requires '{required_permission}' permission. "
f"Your current role(s) ({', '.join(user_roles) if user_roles else 'none'}) "
"do not have access to this feature. Please contact an administrator "
"if you believe you should have access."
)
except Exception as e:
# If RBAC registry is not configured, log warning and allow access
logger.warning(f"RBAC registry not available, allowing tool access: {e}")
return True, None

except ImportError as e:
# Flask not available (e.g., running outside web context)
logger.debug(f"Flask not available, allowing tool access: {e}")
return True, None
except Exception as e:
logger.error(f"Unexpected error checking tool permission: {e}")
# Fail open for unexpected errors to avoid breaking functionality
return True, None


def require_tool_permission(permission: Optional[str]) -> Callable[[F], F]:
"""
Decorator that enforces RBAC permission check before tool execution.

This decorator wraps a tool function and checks if the current user
has the required permission before allowing the tool to execute.
If permission is denied, returns an error message instead of executing the tool.

Args:
permission: The permission string required to use the tool (e.g., 'tools:http_get').
If None, no permission check is performed (allow all).

Returns:
A decorator function that wraps the tool with permission checking.

Example:
@require_tool_permission("tools:http_get")
def _http_get_tool(url: str) -> str:
...

Note:
- If permission is None, the decorator is a no-op (returns original function)
- Permission checks fail open in non-web contexts (CLI, testing)
- Permission checks fail open if RBAC registry is not configured
"""
def decorator(func: F) -> F:
if permission is None:
# No permission required, return original function
return func

# Capture permission in closure for type checker (guaranteed non-None here)
required_perm: str = permission

@wraps(func)
def wrapper(*args, **kwargs):
has_perm, error_msg = check_tool_permission(required_perm)
if not has_perm:
logger.warning(f"Tool '{func.__name__}' permission denied: {required_perm}")
return f"Error: {error_msg}"
return func(*args, **kwargs)

return wrapper # type: ignore

return decorator


def create_abstract_tool(
*,
Expand Down
Loading
Loading