replicantx 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.
- replicantx/__init__.py +45 -0
- replicantx/auth/__init__.py +20 -0
- replicantx/auth/base.py +74 -0
- replicantx/auth/jwt.py +102 -0
- replicantx/auth/noop.py +49 -0
- replicantx/auth/supabase.py +147 -0
- replicantx/cli.py +457 -0
- replicantx/models.py +250 -0
- replicantx/reporters/__init__.py +16 -0
- replicantx/reporters/json.py +236 -0
- replicantx/reporters/markdown.py +298 -0
- replicantx/scenarios/__init__.py +18 -0
- replicantx/scenarios/agent.py +596 -0
- replicantx/scenarios/basic.py +379 -0
- replicantx/scenarios/replicant.py +316 -0
- replicantx/tools/__init__.py +15 -0
- replicantx/tools/http_client.py +341 -0
- replicantx-0.1.0.dist-info/METADATA +792 -0
- replicantx-0.1.0.dist-info/RECORD +23 -0
- replicantx-0.1.0.dist-info/WHEEL +5 -0
- replicantx-0.1.0.dist-info/entry_points.txt +2 -0
- replicantx-0.1.0.dist-info/licenses/LICENSE +194 -0
- replicantx-0.1.0.dist-info/top_level.txt +1 -0
replicantx/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Copyright 2025 Helix Technologies Limited
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (see LICENSE file).
|
|
3
|
+
"""
|
|
4
|
+
ReplicantX - End-to-end testing harness for AI agents via web service APIs.
|
|
5
|
+
|
|
6
|
+
This package provides tools for testing AI agents by calling their HTTP APIs
|
|
7
|
+
with configurable authentication, assertions, and reporting.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__author__ = "ReplicantX Team"
|
|
12
|
+
__email__ = "team@replicantx.ai"
|
|
13
|
+
|
|
14
|
+
from .models import (
|
|
15
|
+
Message,
|
|
16
|
+
Step,
|
|
17
|
+
StepResult,
|
|
18
|
+
ScenarioConfig,
|
|
19
|
+
ScenarioReport,
|
|
20
|
+
AuthConfig,
|
|
21
|
+
AssertionResult,
|
|
22
|
+
TestSuiteReport,
|
|
23
|
+
AuthProvider,
|
|
24
|
+
TestLevel,
|
|
25
|
+
AssertionType,
|
|
26
|
+
ReplicantConfig,
|
|
27
|
+
LLMConfig,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"__version__",
|
|
32
|
+
"Message",
|
|
33
|
+
"Step",
|
|
34
|
+
"StepResult",
|
|
35
|
+
"ScenarioConfig",
|
|
36
|
+
"ScenarioReport",
|
|
37
|
+
"AuthConfig",
|
|
38
|
+
"AssertionResult",
|
|
39
|
+
"TestSuiteReport",
|
|
40
|
+
"AuthProvider",
|
|
41
|
+
"TestLevel",
|
|
42
|
+
"AssertionType",
|
|
43
|
+
"ReplicantConfig",
|
|
44
|
+
"LLMConfig",
|
|
45
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright 2025 Helix Technologies Limited
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (see LICENSE file).
|
|
3
|
+
"""
|
|
4
|
+
Authentication module for ReplicantX.
|
|
5
|
+
|
|
6
|
+
This module provides authentication providers for different services including
|
|
7
|
+
Supabase, JWT, and no-auth options.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .base import AuthBase
|
|
11
|
+
from .supabase import SupabaseAuth
|
|
12
|
+
from .jwt import JWTAuth
|
|
13
|
+
from .noop import NoopAuth
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AuthBase",
|
|
17
|
+
"SupabaseAuth",
|
|
18
|
+
"JWTAuth",
|
|
19
|
+
"NoopAuth",
|
|
20
|
+
]
|
replicantx/auth/base.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Copyright 2025 Helix Technologies Limited
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (see LICENSE file).
|
|
3
|
+
"""
|
|
4
|
+
Base authentication class for ReplicantX.
|
|
5
|
+
|
|
6
|
+
This module defines the abstract base class that all authentication providers
|
|
7
|
+
must inherit from to provide a consistent interface.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import Dict, Optional
|
|
12
|
+
|
|
13
|
+
from ..models import AuthConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthBase(ABC):
|
|
17
|
+
"""Abstract base class for authentication providers."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: AuthConfig):
|
|
20
|
+
"""Initialize the authentication provider.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config: Authentication configuration
|
|
24
|
+
"""
|
|
25
|
+
self.config = config
|
|
26
|
+
self._token: Optional[str] = None
|
|
27
|
+
self._headers: Dict[str, str] = {}
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def authenticate(self) -> str:
|
|
31
|
+
"""Authenticate and return a token.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Authentication token
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
AuthenticationError: If authentication fails
|
|
38
|
+
"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def get_headers(self) -> Dict[str, str]:
|
|
43
|
+
"""Get authentication headers for HTTP requests.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dictionary of headers to include in requests
|
|
47
|
+
"""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
async def token(self) -> str:
|
|
51
|
+
"""Get the current authentication token.
|
|
52
|
+
|
|
53
|
+
This method caches the token and only re-authenticates if necessary.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Current authentication token
|
|
57
|
+
"""
|
|
58
|
+
if self._token is None:
|
|
59
|
+
self._token = await self.authenticate()
|
|
60
|
+
return self._token
|
|
61
|
+
|
|
62
|
+
def invalidate_token(self) -> None:
|
|
63
|
+
"""Invalidate the current token, forcing re-authentication on next request."""
|
|
64
|
+
self._token = None
|
|
65
|
+
self._headers.clear()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AuthenticationError(Exception):
|
|
69
|
+
"""Raised when authentication fails."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, message: str, provider: str):
|
|
72
|
+
self.message = message
|
|
73
|
+
self.provider = provider
|
|
74
|
+
super().__init__(f"Authentication failed for {provider}: {message}")
|
replicantx/auth/jwt.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Copyright 2025 Helix Technologies Limited
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (see LICENSE file).
|
|
3
|
+
"""
|
|
4
|
+
JWT authentication provider for ReplicantX.
|
|
5
|
+
|
|
6
|
+
This module provides authentication using pre-minted JWT tokens,
|
|
7
|
+
typically provided via environment variables or configuration.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from typing import Dict
|
|
12
|
+
|
|
13
|
+
from .base import AuthBase, AuthenticationError
|
|
14
|
+
from ..models import AuthConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JWTAuth(AuthBase):
|
|
18
|
+
"""JWT authentication provider using pre-minted tokens."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: AuthConfig):
|
|
21
|
+
"""Initialize JWT authentication.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
config: Authentication configuration with JWT token
|
|
25
|
+
"""
|
|
26
|
+
super().__init__(config)
|
|
27
|
+
|
|
28
|
+
def _substitute_env_vars(self, value: str) -> str:
|
|
29
|
+
"""Substitute environment variables in string values.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
value: String that may contain {{ env.VAR_NAME }} patterns
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
String with environment variables substituted
|
|
36
|
+
"""
|
|
37
|
+
if not value:
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
# Simple template substitution for {{ env.VAR_NAME }}
|
|
41
|
+
import re
|
|
42
|
+
def replace_env_var(match):
|
|
43
|
+
var_name = match.group(1)
|
|
44
|
+
env_value = os.getenv(var_name)
|
|
45
|
+
if env_value is None:
|
|
46
|
+
raise ValueError(f"Environment variable {var_name} not found")
|
|
47
|
+
return env_value
|
|
48
|
+
|
|
49
|
+
return re.sub(r'\{\{\s*env\.([A-Z_]+)\s*\}\}', replace_env_var, value)
|
|
50
|
+
|
|
51
|
+
async def authenticate(self) -> str:
|
|
52
|
+
"""Return the JWT token.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
JWT token for API requests
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
AuthenticationError: If token is missing or invalid
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
if not self.config.token:
|
|
62
|
+
raise AuthenticationError(
|
|
63
|
+
"JWT token not provided in configuration",
|
|
64
|
+
"jwt"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Substitute environment variables in token
|
|
68
|
+
token = self._substitute_env_vars(self.config.token)
|
|
69
|
+
|
|
70
|
+
if not token:
|
|
71
|
+
raise AuthenticationError(
|
|
72
|
+
"JWT token is empty after environment variable substitution",
|
|
73
|
+
"jwt"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return token
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
if isinstance(e, AuthenticationError):
|
|
80
|
+
raise
|
|
81
|
+
raise AuthenticationError(
|
|
82
|
+
f"JWT authentication failed: {str(e)}",
|
|
83
|
+
"jwt"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def get_headers(self) -> Dict[str, str]:
|
|
87
|
+
"""Get authentication headers for HTTP requests.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dictionary with Authorization header
|
|
91
|
+
"""
|
|
92
|
+
token = await self.token()
|
|
93
|
+
headers = {
|
|
94
|
+
"Authorization": f"Bearer {token}",
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Add any additional headers from config
|
|
99
|
+
if self.config.headers:
|
|
100
|
+
headers.update(self.config.headers)
|
|
101
|
+
|
|
102
|
+
return headers
|
replicantx/auth/noop.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright 2025 Helix Technologies Limited
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (see LICENSE file).
|
|
3
|
+
"""
|
|
4
|
+
No-op authentication provider for ReplicantX.
|
|
5
|
+
|
|
6
|
+
This module provides a no-authentication provider for testing purposes
|
|
7
|
+
or when working with APIs that don't require authentication.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Dict
|
|
11
|
+
|
|
12
|
+
from .base import AuthBase
|
|
13
|
+
from ..models import AuthConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NoopAuth(AuthBase):
|
|
17
|
+
"""No-op authentication provider that provides no authentication."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: AuthConfig):
|
|
20
|
+
"""Initialize noop authentication.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config: Authentication configuration (not used for noop)
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(config)
|
|
26
|
+
|
|
27
|
+
async def authenticate(self) -> str:
|
|
28
|
+
"""Return empty token for no authentication.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Empty string (no token needed)
|
|
32
|
+
"""
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
async def get_headers(self) -> Dict[str, str]:
|
|
36
|
+
"""Get authentication headers for HTTP requests.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dictionary with basic headers (no authentication)
|
|
40
|
+
"""
|
|
41
|
+
headers = {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Add any additional headers from config
|
|
46
|
+
if self.config.headers:
|
|
47
|
+
headers.update(self.config.headers)
|
|
48
|
+
|
|
49
|
+
return headers
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Copyright 2025 Helix Technologies Limited
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (see LICENSE file).
|
|
3
|
+
"""
|
|
4
|
+
Supabase authentication provider for ReplicantX.
|
|
5
|
+
|
|
6
|
+
This module provides authentication via Supabase's email/password flow,
|
|
7
|
+
managing session tokens and providing them as Bearer tokens for API requests.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from typing import Dict
|
|
12
|
+
|
|
13
|
+
from supabase import create_client, Client
|
|
14
|
+
|
|
15
|
+
from .base import AuthBase, AuthenticationError
|
|
16
|
+
from ..models import AuthConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SupabaseAuth(AuthBase):
|
|
20
|
+
"""Supabase authentication provider using email/password."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: AuthConfig):
|
|
23
|
+
"""Initialize Supabase authentication.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Authentication configuration with Supabase credentials
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(config)
|
|
29
|
+
self._client: Client = None
|
|
30
|
+
self._session = None
|
|
31
|
+
|
|
32
|
+
def _get_client(self) -> Client:
|
|
33
|
+
"""Get or create Supabase client.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Supabase client instance
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
AuthenticationError: If client creation fails
|
|
40
|
+
"""
|
|
41
|
+
if self._client is None:
|
|
42
|
+
try:
|
|
43
|
+
# Template substitution for environment variables
|
|
44
|
+
project_url = self._substitute_env_vars(self.config.project_url)
|
|
45
|
+
api_key = self._substitute_env_vars(self.config.api_key)
|
|
46
|
+
|
|
47
|
+
self._client = create_client(project_url, api_key)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
raise AuthenticationError(
|
|
50
|
+
f"Failed to create Supabase client: {str(e)}",
|
|
51
|
+
"supabase"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return self._client
|
|
55
|
+
|
|
56
|
+
def _substitute_env_vars(self, value: str) -> str:
|
|
57
|
+
"""Substitute environment variables in string values.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
value: String that may contain {{ env.VAR_NAME }} patterns
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
String with environment variables substituted
|
|
64
|
+
"""
|
|
65
|
+
if not value:
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
# Simple template substitution for {{ env.VAR_NAME }}
|
|
69
|
+
import re
|
|
70
|
+
def replace_env_var(match):
|
|
71
|
+
var_name = match.group(1)
|
|
72
|
+
env_value = os.getenv(var_name)
|
|
73
|
+
if env_value is None:
|
|
74
|
+
raise ValueError(f"Environment variable {var_name} not found")
|
|
75
|
+
return env_value
|
|
76
|
+
|
|
77
|
+
return re.sub(r'\{\{\s*env\.([A-Z_]+)\s*\}\}', replace_env_var, value)
|
|
78
|
+
|
|
79
|
+
async def authenticate(self) -> str:
|
|
80
|
+
"""Authenticate with Supabase using email/password.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Access token for API requests
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
AuthenticationError: If authentication fails
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
client = self._get_client()
|
|
90
|
+
|
|
91
|
+
# Substitute environment variables in credentials
|
|
92
|
+
email = self._substitute_env_vars(self.config.email)
|
|
93
|
+
password = self._substitute_env_vars(self.config.password)
|
|
94
|
+
|
|
95
|
+
# Sign in with email/password
|
|
96
|
+
auth_response = client.auth.sign_in_with_password({
|
|
97
|
+
"email": email,
|
|
98
|
+
"password": password
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if not auth_response.session:
|
|
102
|
+
raise AuthenticationError(
|
|
103
|
+
"No session returned from Supabase authentication",
|
|
104
|
+
"supabase"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
self._session = auth_response.session
|
|
108
|
+
return auth_response.session.access_token
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
if isinstance(e, AuthenticationError):
|
|
112
|
+
raise
|
|
113
|
+
raise AuthenticationError(
|
|
114
|
+
f"Supabase authentication failed: {str(e)}",
|
|
115
|
+
"supabase"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def get_headers(self) -> Dict[str, str]:
|
|
119
|
+
"""Get authentication headers for HTTP requests.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Dictionary with Authorization header
|
|
123
|
+
"""
|
|
124
|
+
token = await self.token()
|
|
125
|
+
headers = {
|
|
126
|
+
"Authorization": f"Bearer {token}",
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Add any additional headers from config
|
|
131
|
+
if self.config.headers:
|
|
132
|
+
headers.update(self.config.headers)
|
|
133
|
+
|
|
134
|
+
return headers
|
|
135
|
+
|
|
136
|
+
def invalidate_token(self) -> None:
|
|
137
|
+
"""Invalidate current session and token."""
|
|
138
|
+
super().invalidate_token()
|
|
139
|
+
self._session = None
|
|
140
|
+
|
|
141
|
+
# Sign out from Supabase if we have a client
|
|
142
|
+
if self._client:
|
|
143
|
+
try:
|
|
144
|
+
self._client.auth.sign_out()
|
|
145
|
+
except Exception:
|
|
146
|
+
# Ignore errors during sign out
|
|
147
|
+
pass
|