agentsid 0.1.0__tar.gz
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.
- agentsid-0.1.0/.gitignore +39 -0
- agentsid-0.1.0/PKG-INFO +42 -0
- agentsid-0.1.0/README.md +32 -0
- agentsid-0.1.0/agentsid/__init__.py +39 -0
- agentsid-0.1.0/agentsid/client.py +164 -0
- agentsid-0.1.0/agentsid/errors.py +30 -0
- agentsid-0.1.0/agentsid/middleware.py +124 -0
- agentsid-0.1.0/pyproject.toml +16 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
.venv/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
|
|
12
|
+
# Node
|
|
13
|
+
node_modules/
|
|
14
|
+
|
|
15
|
+
# Environment / Secrets — NEVER commit these
|
|
16
|
+
.env
|
|
17
|
+
.env.local
|
|
18
|
+
.env.production
|
|
19
|
+
!.env.example
|
|
20
|
+
*.pem
|
|
21
|
+
*.key
|
|
22
|
+
*.p12
|
|
23
|
+
|
|
24
|
+
# IDE
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
*.swp
|
|
28
|
+
*.swo
|
|
29
|
+
*~
|
|
30
|
+
|
|
31
|
+
# OS
|
|
32
|
+
.DS_Store
|
|
33
|
+
Thumbs.db
|
|
34
|
+
|
|
35
|
+
# Local config (may contain API keys)
|
|
36
|
+
.agentsid/
|
|
37
|
+
|
|
38
|
+
# Logs
|
|
39
|
+
*.log
|
agentsid-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentsid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Identity and auth for AI agents — drop-in MCP middleware
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: agents,ai,auth,identity,mcp,security
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: httpx>=0.25.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# agentsid
|
|
12
|
+
|
|
13
|
+
Identity and auth for AI agents. Official Python SDK for [agentsid.dev](https://agentsid.dev).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install agentsid
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from agentsid import AgentsID, create_mcp_middleware
|
|
25
|
+
|
|
26
|
+
aid = AgentsID(project_key="aid_proj_...")
|
|
27
|
+
|
|
28
|
+
result = await aid.register_agent(
|
|
29
|
+
name="research-bot",
|
|
30
|
+
on_behalf_of="user_123",
|
|
31
|
+
permissions=["search_*", "save_memory"],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
middleware = create_mcp_middleware(project_key="aid_proj_...")
|
|
35
|
+
allowed = await middleware.is_allowed(token, "save_memory") # True
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Links
|
|
39
|
+
|
|
40
|
+
- [Documentation](https://agentsid.dev/docs)
|
|
41
|
+
- [Dashboard](https://agentsid.dev/dashboard)
|
|
42
|
+
- [GitHub](https://github.com/stevenkozeniesky02/agentsid)
|
agentsid-0.1.0/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# agentsid
|
|
2
|
+
|
|
3
|
+
Identity and auth for AI agents. Official Python SDK for [agentsid.dev](https://agentsid.dev).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agentsid
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from agentsid import AgentsID, create_mcp_middleware
|
|
15
|
+
|
|
16
|
+
aid = AgentsID(project_key="aid_proj_...")
|
|
17
|
+
|
|
18
|
+
result = await aid.register_agent(
|
|
19
|
+
name="research-bot",
|
|
20
|
+
on_behalf_of="user_123",
|
|
21
|
+
permissions=["search_*", "save_memory"],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
middleware = create_mcp_middleware(project_key="aid_proj_...")
|
|
25
|
+
allowed = await middleware.is_allowed(token, "save_memory") # True
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Links
|
|
29
|
+
|
|
30
|
+
- [Documentation](https://agentsid.dev/docs)
|
|
31
|
+
- [Dashboard](https://agentsid.dev/dashboard)
|
|
32
|
+
- [GitHub](https://github.com/stevenkozeniesky02/agentsid)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""AgentsID — Identity and auth for AI agents.
|
|
2
|
+
|
|
3
|
+
Quick start:
|
|
4
|
+
|
|
5
|
+
from agentsid import AgentsID
|
|
6
|
+
|
|
7
|
+
aid = AgentsID(project_key="aid_proj_...")
|
|
8
|
+
|
|
9
|
+
# Register an agent
|
|
10
|
+
result = await aid.register_agent(
|
|
11
|
+
name="research-bot",
|
|
12
|
+
on_behalf_of="user_123",
|
|
13
|
+
permissions=["search_memories", "save_memory"],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Validate token + check permission
|
|
17
|
+
check = await aid.validate_token(result["token"], tool="search_memories")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from agentsid.client import AgentsID
|
|
21
|
+
from agentsid.errors import (
|
|
22
|
+
AgentsIDError,
|
|
23
|
+
AuthenticationError,
|
|
24
|
+
PermissionDeniedError,
|
|
25
|
+
TokenExpiredError,
|
|
26
|
+
TokenRevokedError,
|
|
27
|
+
)
|
|
28
|
+
from agentsid.middleware import create_mcp_middleware, validate_tool_call
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"AgentsID",
|
|
32
|
+
"AgentsIDError",
|
|
33
|
+
"AuthenticationError",
|
|
34
|
+
"PermissionDeniedError",
|
|
35
|
+
"TokenExpiredError",
|
|
36
|
+
"TokenRevokedError",
|
|
37
|
+
"create_mcp_middleware",
|
|
38
|
+
"validate_tool_call",
|
|
39
|
+
]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""AgentsID Python SDK — main client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from agentsid.errors import AgentsIDError, AuthenticationError
|
|
8
|
+
|
|
9
|
+
DEFAULT_BASE_URL = "https://agentsid.dev"
|
|
10
|
+
DEFAULT_TIMEOUT = 10.0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentsID:
|
|
14
|
+
"""AgentsID client — register agents, validate tokens, manage permissions."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
project_key: str,
|
|
19
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
20
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
21
|
+
) -> None:
|
|
22
|
+
if not project_key:
|
|
23
|
+
raise AgentsIDError("project_key is required", "CONFIG_ERROR")
|
|
24
|
+
self._project_key = project_key
|
|
25
|
+
self._base_url = base_url.rstrip("/")
|
|
26
|
+
self._timeout = timeout
|
|
27
|
+
|
|
28
|
+
# ═══════════════════════════════════════════
|
|
29
|
+
# AGENTS
|
|
30
|
+
# ═══════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
async def register_agent(
|
|
33
|
+
self,
|
|
34
|
+
name: str,
|
|
35
|
+
on_behalf_of: str,
|
|
36
|
+
permissions: list[str] | None = None,
|
|
37
|
+
ttl_hours: int | None = None,
|
|
38
|
+
metadata: dict | None = None,
|
|
39
|
+
) -> dict:
|
|
40
|
+
"""Register a new agent identity and issue a token."""
|
|
41
|
+
return await self._request("POST", "/agents/", {
|
|
42
|
+
"name": name,
|
|
43
|
+
"on_behalf_of": on_behalf_of,
|
|
44
|
+
"permissions": permissions,
|
|
45
|
+
"ttl_hours": ttl_hours,
|
|
46
|
+
"metadata": metadata,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
async def get_agent(self, agent_id: str) -> dict:
|
|
50
|
+
return await self._request("GET", f"/agents/{agent_id}")
|
|
51
|
+
|
|
52
|
+
async def list_agents(self, status: str | None = None, limit: int = 50) -> list[dict]:
|
|
53
|
+
params = {}
|
|
54
|
+
if status:
|
|
55
|
+
params["status"] = status
|
|
56
|
+
if limit != 50:
|
|
57
|
+
params["limit"] = str(limit)
|
|
58
|
+
qs = "&".join(f"{k}={v}" for k, v in params.items())
|
|
59
|
+
return await self._request("GET", f"/agents/?{qs}" if qs else "/agents/")
|
|
60
|
+
|
|
61
|
+
async def revoke_agent(self, agent_id: str) -> None:
|
|
62
|
+
await self._request("DELETE", f"/agents/{agent_id}")
|
|
63
|
+
|
|
64
|
+
# ═══════════════════════════════════════════
|
|
65
|
+
# PERMISSIONS
|
|
66
|
+
# ═══════════════════════════════════════════
|
|
67
|
+
|
|
68
|
+
async def set_permissions(self, agent_id: str, rules: list[dict]) -> dict:
|
|
69
|
+
"""Set permission rules. Each rule: {"tool_pattern": "...", "action": "allow"|"deny"}"""
|
|
70
|
+
body = [
|
|
71
|
+
{
|
|
72
|
+
"tool_pattern": r.get("tool_pattern", r.get("toolPattern", "")),
|
|
73
|
+
"action": r.get("action", "allow"),
|
|
74
|
+
"conditions": r.get("conditions"),
|
|
75
|
+
"priority": r.get("priority", 0),
|
|
76
|
+
}
|
|
77
|
+
for r in rules
|
|
78
|
+
]
|
|
79
|
+
return await self._request("PUT", f"/agents/{agent_id}/permissions", body)
|
|
80
|
+
|
|
81
|
+
async def get_permissions(self, agent_id: str) -> list[dict]:
|
|
82
|
+
data = await self._request("GET", f"/agents/{agent_id}/permissions")
|
|
83
|
+
return data.get("rules", [])
|
|
84
|
+
|
|
85
|
+
async def check_permission(
|
|
86
|
+
self, agent_id: str, tool: str, params: dict | None = None
|
|
87
|
+
) -> dict:
|
|
88
|
+
return await self._request("POST", "/check", {
|
|
89
|
+
"agent_id": agent_id,
|
|
90
|
+
"tool": tool,
|
|
91
|
+
"params": params,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
# ═══════════════════════════════════════════
|
|
95
|
+
# TOKEN VALIDATION
|
|
96
|
+
# ═══════════════════════════════════════════
|
|
97
|
+
|
|
98
|
+
async def validate_token(
|
|
99
|
+
self, token: str, tool: str | None = None, params: dict | None = None
|
|
100
|
+
) -> dict:
|
|
101
|
+
return await self._request("POST", "/validate", {
|
|
102
|
+
"token": token,
|
|
103
|
+
"tool": tool,
|
|
104
|
+
"params": params,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
# ═══════════════════════════════════════════
|
|
108
|
+
# AUDIT
|
|
109
|
+
# ═══════════════════════════════════════════
|
|
110
|
+
|
|
111
|
+
async def get_audit_log(
|
|
112
|
+
self,
|
|
113
|
+
agent_id: str | None = None,
|
|
114
|
+
tool: str | None = None,
|
|
115
|
+
action: str | None = None,
|
|
116
|
+
since: str | None = None,
|
|
117
|
+
limit: int = 100,
|
|
118
|
+
) -> dict:
|
|
119
|
+
params = {}
|
|
120
|
+
if agent_id:
|
|
121
|
+
params["agent_id"] = agent_id
|
|
122
|
+
if tool:
|
|
123
|
+
params["tool"] = tool
|
|
124
|
+
if action:
|
|
125
|
+
params["action"] = action
|
|
126
|
+
if since:
|
|
127
|
+
params["since"] = since
|
|
128
|
+
params["limit"] = str(limit)
|
|
129
|
+
qs = "&".join(f"{k}={v}" for k, v in params.items())
|
|
130
|
+
return await self._request("GET", f"/audit/?{qs}")
|
|
131
|
+
|
|
132
|
+
# ═══════════════════════════════════════════
|
|
133
|
+
# HTTP CLIENT
|
|
134
|
+
# ═══════════════════════════════════════════
|
|
135
|
+
|
|
136
|
+
async def _request(self, method: str, path: str, body: object | None = None) -> dict:
|
|
137
|
+
url = f"{self._base_url}/api/v1{path}"
|
|
138
|
+
headers = {
|
|
139
|
+
"Authorization": f"Bearer {self._project_key}",
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
|
144
|
+
response = await client.request(
|
|
145
|
+
method, url, headers=headers,
|
|
146
|
+
json=body if body is not None else None,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if response.status_code == 401:
|
|
150
|
+
raise AuthenticationError()
|
|
151
|
+
|
|
152
|
+
if response.status_code == 204:
|
|
153
|
+
return {}
|
|
154
|
+
|
|
155
|
+
data = response.json()
|
|
156
|
+
|
|
157
|
+
if not response.is_success:
|
|
158
|
+
raise AgentsIDError(
|
|
159
|
+
data.get("detail", f"Request failed: {response.status_code}"),
|
|
160
|
+
"API_ERROR",
|
|
161
|
+
response.status_code,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return data
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""AgentsID error classes."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AgentsIDError(Exception):
|
|
5
|
+
def __init__(self, message: str, code: str = "UNKNOWN", status_code: int | None = None):
|
|
6
|
+
super().__init__(message)
|
|
7
|
+
self.code = code
|
|
8
|
+
self.status_code = status_code
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuthenticationError(AgentsIDError):
|
|
12
|
+
def __init__(self, message: str = "Invalid or missing API key"):
|
|
13
|
+
super().__init__(message, "AUTH_ERROR", 401)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PermissionDeniedError(AgentsIDError):
|
|
17
|
+
def __init__(self, tool: str, reason: str):
|
|
18
|
+
super().__init__(f'Permission denied for tool "{tool}": {reason}', "PERMISSION_DENIED", 403)
|
|
19
|
+
self.tool = tool
|
|
20
|
+
self.reason = reason
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TokenExpiredError(AgentsIDError):
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__("Agent token has expired", "TOKEN_EXPIRED", 401)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TokenRevokedError(AgentsIDError):
|
|
29
|
+
def __init__(self):
|
|
30
|
+
super().__init__("Agent token has been revoked", "TOKEN_REVOKED", 401)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""AgentsID MCP middleware for Python.
|
|
2
|
+
|
|
3
|
+
Drop-in middleware for Python MCP servers. Validates agent tokens,
|
|
4
|
+
checks per-tool permissions, and blocks unauthorized calls.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from agentsid import create_mcp_middleware
|
|
8
|
+
|
|
9
|
+
middleware = create_mcp_middleware(project_key="aid_proj_...")
|
|
10
|
+
|
|
11
|
+
# In your MCP tool handler:
|
|
12
|
+
async def my_tool(params, context):
|
|
13
|
+
auth = await middleware.validate(bearer_token, "my_tool", params)
|
|
14
|
+
# auth.permission.allowed is True/False
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
from agentsid.errors import (
|
|
22
|
+
AgentsIDError,
|
|
23
|
+
PermissionDeniedError,
|
|
24
|
+
TokenExpiredError,
|
|
25
|
+
TokenRevokedError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
DEFAULT_BASE_URL = "https://agentsid.dev"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def validate_tool_call(
|
|
32
|
+
project_key: str,
|
|
33
|
+
token: str,
|
|
34
|
+
tool: str,
|
|
35
|
+
params: dict | None = None,
|
|
36
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
37
|
+
) -> dict:
|
|
38
|
+
"""Validate a tool call against AgentsID. Returns validation result."""
|
|
39
|
+
url = f"{base_url.rstrip('/')}/api/v1/validate"
|
|
40
|
+
|
|
41
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
42
|
+
response = await client.post(
|
|
43
|
+
url,
|
|
44
|
+
json={"token": token, "tool": tool, "params": params},
|
|
45
|
+
headers={"Content-Type": "application/json"},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if not response.is_success:
|
|
49
|
+
return {"valid": False, "reason": f"Validation failed: {response.status_code}"}
|
|
50
|
+
|
|
51
|
+
return response.json()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MCPMiddleware:
|
|
55
|
+
"""MCP middleware instance — validates tool calls against AgentsID."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
project_key: str,
|
|
60
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
61
|
+
skip_tools: list[str] | None = None,
|
|
62
|
+
on_denied: object | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
self._project_key = project_key
|
|
65
|
+
self._base_url = base_url.rstrip("/")
|
|
66
|
+
self._skip_tools = set(skip_tools or [])
|
|
67
|
+
self._on_denied = on_denied
|
|
68
|
+
|
|
69
|
+
async def validate(
|
|
70
|
+
self, token: str, tool: str, params: dict | None = None
|
|
71
|
+
) -> dict:
|
|
72
|
+
"""Validate a tool call. Raises on denial unless on_denied is set."""
|
|
73
|
+
if tool in self._skip_tools:
|
|
74
|
+
return {"valid": True, "reason": "Tool in skip list"}
|
|
75
|
+
|
|
76
|
+
result = await validate_tool_call(
|
|
77
|
+
self._project_key, token, tool, params, self._base_url
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if not result.get("valid"):
|
|
81
|
+
reason = result.get("reason", "Unknown")
|
|
82
|
+
if "expired" in reason:
|
|
83
|
+
raise TokenExpiredError()
|
|
84
|
+
if "revoked" in reason:
|
|
85
|
+
raise TokenRevokedError()
|
|
86
|
+
|
|
87
|
+
permission = result.get("permission", {})
|
|
88
|
+
if permission and not permission.get("allowed"):
|
|
89
|
+
reason = permission.get("reason", "Denied")
|
|
90
|
+
if self._on_denied:
|
|
91
|
+
self._on_denied(tool, reason)
|
|
92
|
+
else:
|
|
93
|
+
raise PermissionDeniedError(tool, reason)
|
|
94
|
+
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
async def is_allowed(self, token: str, tool: str) -> bool:
|
|
98
|
+
"""Quick check — returns True/False without raising."""
|
|
99
|
+
try:
|
|
100
|
+
result = await validate_tool_call(
|
|
101
|
+
self._project_key, token, tool, base_url=self._base_url
|
|
102
|
+
)
|
|
103
|
+
return result.get("valid", False) and result.get("permission", {}).get("allowed", False)
|
|
104
|
+
except Exception:
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def create_mcp_middleware(
|
|
109
|
+
project_key: str,
|
|
110
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
111
|
+
skip_tools: list[str] | None = None,
|
|
112
|
+
) -> MCPMiddleware:
|
|
113
|
+
"""Create an MCP middleware instance.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
project_key: Your AgentsID project key (aid_proj_...)
|
|
117
|
+
base_url: AgentsID server URL
|
|
118
|
+
skip_tools: Tool names to skip validation for
|
|
119
|
+
"""
|
|
120
|
+
return MCPMiddleware(
|
|
121
|
+
project_key=project_key,
|
|
122
|
+
base_url=base_url,
|
|
123
|
+
skip_tools=skip_tools,
|
|
124
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agentsid"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Identity and auth for AI agents — drop-in MCP middleware"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = ["httpx>=0.25.0"]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
keywords = ["ai", "agents", "auth", "identity", "mcp", "security"]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["agentsid"]
|