securevector-sdk-crewai 1.2.1__tar.gz → 1.2.2__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.
- {securevector_sdk_crewai-1.2.1/src/securevector_sdk_crewai.egg-info → securevector_sdk_crewai-1.2.2}/PKG-INFO +1 -1
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/pyproject.toml +1 -1
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/__init__.py +2 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/_version.py +1 -1
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/client.py +25 -8
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/config.py +7 -0
- securevector_sdk_crewai-1.2.2/src/securevector_sdk_crewai/tool_id.py +77 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2/src/securevector_sdk_crewai.egg-info}/PKG-INFO +1 -1
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai.egg-info/SOURCES.txt +1 -0
- securevector_sdk_crewai-1.2.2/tests/test_auth.py +57 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/tests/test_tool_id.py +22 -1
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/tests/test_verdict.py +35 -0
- securevector_sdk_crewai-1.2.1/src/securevector_sdk_crewai/tool_id.py +0 -40
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/LICENSE +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/NOTICE +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/README.md +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/setup.cfg +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/auto.py +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/core.py +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/costs.py +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/errors.py +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/wrapper.py +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai.egg-info/dependency_links.txt +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai.egg-info/requires.txt +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai.egg-info/top_level.txt +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/tests/test_analyze.py +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/tests/test_costs.py +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/tests/test_engine_endpoint.py +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/tests/test_interceptor.py +0 -0
- {securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/tests/test_wrapper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: securevector-sdk-crewai
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: SecureVector SDK for CrewAI — brings the local threat monitor's three controls (tool-call permissions, secret/data-leak detection, threat detection) to every CrewAI tool call, with tamper-evident audit logging.
|
|
5
5
|
Author: SecureVector
|
|
6
6
|
License: Apache-2.0
|
|
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
|
|
|
8
8
|
# `crewai-*` prefix that could imply an official CrewAI integration. See README
|
|
9
9
|
# "Trademarks".
|
|
10
10
|
name = "securevector-sdk-crewai"
|
|
11
|
-
version = "1.2.
|
|
11
|
+
version = "1.2.2"
|
|
12
12
|
description = "SecureVector SDK for CrewAI — brings the local threat monitor's three controls (tool-call permissions, secret/data-leak detection, threat detection) to every CrewAI tool call, with tamper-evident audit logging."
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.10"
|
|
@@ -37,6 +37,7 @@ from .config import Config
|
|
|
37
37
|
from .costs import CostTracker, install_kickoff_tracking, track_crew_usage
|
|
38
38
|
from .errors import AppUnreachable, SecureVectorError, ToolBlocked
|
|
39
39
|
from .wrapper import SecureCrew
|
|
40
|
+
from .tool_id import candidate_tool_ids
|
|
40
41
|
|
|
41
42
|
log = logging.getLogger("securevector_sdk_crewai")
|
|
42
43
|
|
|
@@ -48,6 +49,7 @@ __all__ = [
|
|
|
48
49
|
"track_crew_usage",
|
|
49
50
|
"SecureCrew",
|
|
50
51
|
"Config",
|
|
52
|
+
"candidate_tool_ids",
|
|
51
53
|
"SecureVectorError",
|
|
52
54
|
"ToolBlocked",
|
|
53
55
|
"AppUnreachable",
|
|
@@ -26,7 +26,7 @@ from typing import Any, Dict, List, Optional
|
|
|
26
26
|
|
|
27
27
|
from .config import Config
|
|
28
28
|
from .errors import AppUnreachable
|
|
29
|
-
from .tool_id import RUNTIME_KIND
|
|
29
|
+
from .tool_id import RUNTIME_KIND, candidate_tool_ids
|
|
30
30
|
|
|
31
31
|
log = logging.getLogger("securevector_sdk_crewai")
|
|
32
32
|
|
|
@@ -63,11 +63,16 @@ class LocalAppClient:
|
|
|
63
63
|
def _request(self, method: str, path: str, body: Optional[dict]) -> Any:
|
|
64
64
|
url = f"{self.cfg.base_url.rstrip('/')}{path}"
|
|
65
65
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
66
|
+
headers = {"Content-Type": "application/json"}
|
|
67
|
+
# Forward a credential to remote, token-gated deployments (no-op for the
|
|
68
|
+
# default loopback app, which has no inbound auth).
|
|
69
|
+
if getattr(self.cfg, "api_key", ""):
|
|
70
|
+
headers["Authorization"] = f"Bearer {self.cfg.api_key}"
|
|
66
71
|
req = urllib.request.Request(
|
|
67
72
|
url,
|
|
68
73
|
data=data,
|
|
69
74
|
method=method,
|
|
70
|
-
headers=
|
|
75
|
+
headers=headers,
|
|
71
76
|
)
|
|
72
77
|
timeout = max(self.cfg.timeout_ms / 1000.0, 0.1)
|
|
73
78
|
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 (localhost)
|
|
@@ -117,15 +122,27 @@ class LocalAppClient:
|
|
|
117
122
|
out[str(k)] = item
|
|
118
123
|
return out
|
|
119
124
|
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _lookup(index: Dict[str, dict], candidates: List[str]) -> Optional[dict]:
|
|
127
|
+
"""First candidate that matches (exact casing, then lowercased)."""
|
|
128
|
+
for cand in candidates:
|
|
129
|
+
hit = index.get(cand) or index.get(cand.lower())
|
|
130
|
+
if hit:
|
|
131
|
+
return hit
|
|
132
|
+
return None
|
|
133
|
+
|
|
120
134
|
def _resolve(self, tool_id, essential, overrides, synced) -> Verdict:
|
|
121
135
|
name = tool_id
|
|
122
|
-
|
|
136
|
+
# Tier precedence dominates candidate specificity: a synced rule that
|
|
137
|
+
# matches ANY candidate beats an override that matches the raw name.
|
|
138
|
+
candidates = candidate_tool_ids(tool_id)
|
|
123
139
|
emap = self._index(essential.get("tools"), "tool_id")
|
|
124
140
|
omap = self._index(overrides.get("overrides"), "tool_id")
|
|
125
141
|
smap = self._index(synced.get("synced"), "tool_id")
|
|
142
|
+
in_essential = self._lookup(emap, candidates) is not None
|
|
126
143
|
|
|
127
144
|
# 1. Cloud-pushed synced policy wins.
|
|
128
|
-
s =
|
|
145
|
+
s = self._lookup(smap, candidates)
|
|
129
146
|
if s:
|
|
130
147
|
effect = str(s.get("effect", "")).lower()
|
|
131
148
|
action = "allow" if effect == "allow" else "block"
|
|
@@ -133,18 +150,18 @@ class LocalAppClient:
|
|
|
133
150
|
ver = f" v{s['policy_version']}" if s.get("policy_version") is not None else ""
|
|
134
151
|
return Verdict(
|
|
135
152
|
action, "synced", f"Synced policy '{policy}'{ver}: {effect}",
|
|
136
|
-
|
|
153
|
+
in_essential, name,
|
|
137
154
|
)
|
|
138
155
|
# 2. Local user override.
|
|
139
|
-
o =
|
|
156
|
+
o = self._lookup(omap, candidates)
|
|
140
157
|
if o:
|
|
141
158
|
return Verdict(
|
|
142
159
|
o.get("action", "allow"), "overridden",
|
|
143
160
|
f"User override: {o.get('action')}",
|
|
144
|
-
|
|
161
|
+
in_essential, name,
|
|
145
162
|
)
|
|
146
163
|
# 3. Essential registry default.
|
|
147
|
-
e =
|
|
164
|
+
e = self._lookup(emap, candidates)
|
|
148
165
|
if e:
|
|
149
166
|
return Verdict(
|
|
150
167
|
e.get("effective_action") or e.get("default_action") or "allow",
|
|
@@ -17,6 +17,11 @@ Environment variables (all optional):
|
|
|
17
17
|
SECUREVECTOR_SDK_AGENT_ID agent id for Cost Tracking attribution
|
|
18
18
|
(default "<runtime>-agent")
|
|
19
19
|
SECUREVECTOR_SDK_DISABLED set truthy to no-op entirely
|
|
20
|
+
SECUREVECTOR_API_KEY credential forwarded to the app as
|
|
21
|
+
Authorization: Bearer — required when the
|
|
22
|
+
app is a remote, token-gated deployment
|
|
23
|
+
(e.g. the Terraform self-host modules);
|
|
24
|
+
unused for the default loopback app
|
|
20
25
|
|
|
21
26
|
Note: SECUREVECTOR_ENGINE_ENDPOINT (and its legacy alias
|
|
22
27
|
SECUREVECTOR_SDK_APP_URL) address the *engine* — the local app or a self-host
|
|
@@ -43,6 +48,7 @@ class Config:
|
|
|
43
48
|
analyze_mode: str = "local" # SecureVectorClient mode used for /analyze
|
|
44
49
|
agent_id: str = "" # Cost Tracking attribution ("" → "<runtime>-agent")
|
|
45
50
|
enabled: bool = True
|
|
51
|
+
api_key: str = "" # forwarded as Authorization: Bearer to the app
|
|
46
52
|
|
|
47
53
|
@classmethod
|
|
48
54
|
def from_env(cls, **overrides) -> "Config":
|
|
@@ -57,6 +63,7 @@ class Config:
|
|
|
57
63
|
analyze_mode=os.environ.get("SECUREVECTOR_SDK_ANALYZE_MODE", "local"),
|
|
58
64
|
agent_id=os.environ.get("SECUREVECTOR_SDK_AGENT_ID", ""),
|
|
59
65
|
enabled=not _truthy(os.environ.get("SECUREVECTOR_SDK_DISABLED", "")),
|
|
66
|
+
api_key=os.environ.get("SECUREVECTOR_API_KEY", ""),
|
|
60
67
|
)
|
|
61
68
|
# Explicit kwargs win over env, but only when actually provided.
|
|
62
69
|
for key, value in overrides.items():
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Canonical tool-id normalization — the ONLY framework-specific mapping.
|
|
2
|
+
|
|
3
|
+
The whole SecureVector fleet keys permissions and audit on a single canonical
|
|
4
|
+
``tool_id``. CrewAI surfaces a tool's name as ``BaseTool.name`` (passed here as
|
|
5
|
+
the flat ``name`` argument). Normalizing it here is the one piece of
|
|
6
|
+
CrewAI-specific code; every downstream step (permission resolution, analysis,
|
|
7
|
+
audit) is shared engine behaviour, so a policy authored once — "allow
|
|
8
|
+
web_search, block shell" — applies identically across LangChain / LangGraph /
|
|
9
|
+
CrewAI.
|
|
10
|
+
|
|
11
|
+
Casing is preserved: the local app matches tool ids case-insensitively, so a
|
|
12
|
+
rule authored ``tool_id="Bash"`` still governs a CrewAI tool named ``bash``.
|
|
13
|
+
|
|
14
|
+
MCP tools commonly surface with a sanitized flat name (``mcp_<server>_<tool>``,
|
|
15
|
+
hyphens/dots collapsed to underscores). The sanitization is lossy, so a single
|
|
16
|
+
function name cannot be split back into ``server``/``tool`` unambiguously.
|
|
17
|
+
``candidate_tool_ids`` therefore emits every plausible ``<server>:<tool>``
|
|
18
|
+
split (most-specific first) so rules authored against the cloud
|
|
19
|
+
``<server>:<tool>`` form, the bare tool name, or the raw function name all
|
|
20
|
+
match — identical behaviour across the sibling SDKs.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import Any, List, Optional
|
|
24
|
+
|
|
25
|
+
# The audit/Bill-of-Tools/OCSF pipeline groups by this attribution tag.
|
|
26
|
+
RUNTIME_KIND = "crewai"
|
|
27
|
+
|
|
28
|
+
_MCP_PREFIX = "mcp_"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def normalize_tool_id(serialized: Any, name: Optional[str] = None) -> str:
|
|
32
|
+
"""Resolve a canonical tool id.
|
|
33
|
+
|
|
34
|
+
Accepts a flat ``name`` (CrewAI ``BaseTool.name``) and/or a ``serialized``
|
|
35
|
+
dict (``{"name": ...}`` or a dotted ``{"id": [...]}`` path) for symmetry
|
|
36
|
+
with the other SDKs. Falls back to ``"unknown"`` so a missing name never
|
|
37
|
+
crashes the agent.
|
|
38
|
+
"""
|
|
39
|
+
raw: Optional[str] = None
|
|
40
|
+
if isinstance(serialized, dict):
|
|
41
|
+
raw = serialized.get("name")
|
|
42
|
+
if not raw:
|
|
43
|
+
ident = serialized.get("id")
|
|
44
|
+
if isinstance(ident, (list, tuple)) and ident:
|
|
45
|
+
raw = str(ident[-1])
|
|
46
|
+
if not raw:
|
|
47
|
+
raw = name
|
|
48
|
+
if not raw:
|
|
49
|
+
return "unknown"
|
|
50
|
+
return str(raw).strip() or "unknown"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def candidate_tool_ids(tool_id: str) -> List[str]:
|
|
54
|
+
"""Expand one tool name into every rule key it should match.
|
|
55
|
+
|
|
56
|
+
Ordered most-specific first (the caller resolves tier-first, then takes
|
|
57
|
+
the first candidate that matches within a tier):
|
|
58
|
+
|
|
59
|
+
* the raw tool name itself (``mcp_my_api_list_items`` or ``terminal``);
|
|
60
|
+
* for MCP names, every ``<server>:<tool>`` split of the sanitized
|
|
61
|
+
remainder — the cloud policy form. The split point is ambiguous after
|
|
62
|
+
sanitization, so all splits are emitted; the app also aliases the bare
|
|
63
|
+
tool suffix of each ``<server>:<tool>`` key server-side.
|
|
64
|
+
"""
|
|
65
|
+
# The caller passes an already-normalized name (normalize_tool_id here
|
|
66
|
+
# takes the framework payload, not a bare string — unlike the hermes SDK).
|
|
67
|
+
tid = str(tool_id or "").strip() or "unknown"
|
|
68
|
+
candidates = [tid]
|
|
69
|
+
if tid.lower().startswith(_MCP_PREFIX):
|
|
70
|
+
rest = tid[len(_MCP_PREFIX):]
|
|
71
|
+
parts = rest.split("_")
|
|
72
|
+
for i in range(1, len(parts)):
|
|
73
|
+
server = "_".join(parts[:i])
|
|
74
|
+
tool = "_".join(parts[i:])
|
|
75
|
+
if server and tool:
|
|
76
|
+
candidates.append(f"{server}:{tool}")
|
|
77
|
+
return candidates
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: securevector-sdk-crewai
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: SecureVector SDK for CrewAI — brings the local threat monitor's three controls (tool-call permissions, secret/data-leak detection, threat detection) to every CrewAI tool call, with tamper-evident audit logging.
|
|
5
5
|
Author: SecureVector
|
|
6
6
|
License: Apache-2.0
|
|
@@ -18,6 +18,7 @@ src/securevector_sdk_crewai.egg-info/dependency_links.txt
|
|
|
18
18
|
src/securevector_sdk_crewai.egg-info/requires.txt
|
|
19
19
|
src/securevector_sdk_crewai.egg-info/top_level.txt
|
|
20
20
|
tests/test_analyze.py
|
|
21
|
+
tests/test_auth.py
|
|
21
22
|
tests/test_costs.py
|
|
22
23
|
tests/test_engine_endpoint.py
|
|
23
24
|
tests/test_interceptor.py
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Auth-forwarding: SECUREVECTOR_API_KEY -> Authorization: Bearer.
|
|
2
|
+
|
|
3
|
+
The SDK talks to the local app over loopback by default (no auth). When the app
|
|
4
|
+
is a remote, token-gated deployment (e.g. the Terraform self-host modules), the
|
|
5
|
+
client forwards SECUREVECTOR_API_KEY as a Bearer credential. These tests pin
|
|
6
|
+
that contract without needing a running app.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
from securevector_sdk_crewai.client import LocalAppClient
|
|
12
|
+
from securevector_sdk_crewai.config import Config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _FakeResp:
|
|
16
|
+
def __init__(self, payload=b"{}"):
|
|
17
|
+
self._payload = payload
|
|
18
|
+
|
|
19
|
+
def read(self):
|
|
20
|
+
return self._payload
|
|
21
|
+
|
|
22
|
+
def __enter__(self):
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def __exit__(self, *exc):
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _capture_headers(monkeypatch):
|
|
30
|
+
captured = {}
|
|
31
|
+
|
|
32
|
+
def fake_urlopen(req, timeout=None):
|
|
33
|
+
captured.update({k.lower(): v for k, v in req.header_items()})
|
|
34
|
+
return _FakeResp()
|
|
35
|
+
|
|
36
|
+
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
|
|
37
|
+
return captured
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_api_key_forwarded_as_bearer(monkeypatch):
|
|
41
|
+
headers = _capture_headers(monkeypatch)
|
|
42
|
+
LocalAppClient(Config.from_env(api_key="svpk_explicit"))._get("/health")
|
|
43
|
+
assert headers.get("authorization") == "Bearer svpk_explicit"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_api_key_read_from_env(monkeypatch):
|
|
47
|
+
monkeypatch.setenv("SECUREVECTOR_API_KEY", "svpk_fromenv")
|
|
48
|
+
headers = _capture_headers(monkeypatch)
|
|
49
|
+
LocalAppClient(Config.from_env())._get("/health")
|
|
50
|
+
assert headers.get("authorization") == "Bearer svpk_fromenv"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_no_auth_header_when_unset(monkeypatch):
|
|
54
|
+
monkeypatch.delenv("SECUREVECTOR_API_KEY", raising=False)
|
|
55
|
+
headers = _capture_headers(monkeypatch)
|
|
56
|
+
LocalAppClient(Config.from_env(api_key=""))._get("/health")
|
|
57
|
+
assert "authorization" not in headers
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
from securevector_sdk_crewai.tool_id import
|
|
1
|
+
from securevector_sdk_crewai.tool_id import (
|
|
2
|
+
RUNTIME_KIND,
|
|
3
|
+
candidate_tool_ids,
|
|
4
|
+
normalize_tool_id,
|
|
5
|
+
)
|
|
2
6
|
|
|
3
7
|
|
|
4
8
|
def test_runtime_kind_is_langchain():
|
|
@@ -25,3 +29,20 @@ def test_missing_everything_is_unknown():
|
|
|
25
29
|
assert normalize_tool_id(None) == "unknown"
|
|
26
30
|
assert normalize_tool_id({}) == "unknown"
|
|
27
31
|
assert normalize_tool_id({"name": " "}) == "unknown"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_non_mcp_candidates_are_just_the_name():
|
|
35
|
+
assert candidate_tool_ids("terminal") == ["terminal"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_mcp_candidates_emit_every_server_tool_split():
|
|
39
|
+
cands = candidate_tool_ids("mcp_my_api_list_items")
|
|
40
|
+
assert cands[0] == "mcp_my_api_list_items" # raw name is most specific
|
|
41
|
+
assert "my:api_list_items" in cands
|
|
42
|
+
assert "my_api:list_items" in cands
|
|
43
|
+
assert "my_api_list:items" in cands
|
|
44
|
+
assert len(cands) == 4
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_mcp_single_segment_has_no_split():
|
|
48
|
+
assert candidate_tool_ids("mcp_solo") == ["mcp_solo"]
|
|
@@ -54,3 +54,38 @@ def test_synced_non_allow_effect_falls_back_to_block():
|
|
|
54
54
|
synced = {"synced": [{"tool_id": "x", "effect": "warn"}]}
|
|
55
55
|
v = _client()._resolve("x", {}, {}, synced)
|
|
56
56
|
assert v.action == "block" # engine has no warn action → safer block
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# --- MCP candidate matching ---------------------------------------------- #
|
|
60
|
+
# mcp_<server>_<tool> is lossy; a rule authored in the cloud <server>:<tool>
|
|
61
|
+
# form must still govern the call.
|
|
62
|
+
|
|
63
|
+
def test_mcp_call_matches_cloud_server_tool_rule():
|
|
64
|
+
synced = {"synced": [{"tool_id": "github:create_issue", "effect": "block", "policy_name": "corp"}]}
|
|
65
|
+
v = _client()._resolve("mcp_github_create_issue", {}, {}, synced)
|
|
66
|
+
assert v.action == "block"
|
|
67
|
+
assert v.risk == "synced"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_mcp_call_matches_raw_function_name_rule():
|
|
71
|
+
overrides = {"overrides": [{"tool_id": "mcp_github_create_issue", "action": "block"}]}
|
|
72
|
+
v = _client()._resolve("mcp_github_create_issue", {}, overrides, {})
|
|
73
|
+
assert v.action == "block"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_mcp_ambiguous_split_still_matches_underscored_server():
|
|
77
|
+
# server "my-api" sanitized to my_api: rule my_api:list_items must match
|
|
78
|
+
# mcp_my_api_list_items even though the split point is ambiguous.
|
|
79
|
+
synced = {"synced": [{"tool_id": "my_api:list_items", "effect": "block"}]}
|
|
80
|
+
v = _client()._resolve("mcp_my_api_list_items", {}, {}, synced)
|
|
81
|
+
assert v.action == "block"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_tier_precedence_dominates_candidate_specificity():
|
|
85
|
+
# A synced rule matching a LESS specific candidate still beats an
|
|
86
|
+
# override matching the raw name.
|
|
87
|
+
overrides = {"overrides": [{"tool_id": "mcp_github_create_issue", "action": "allow"}]}
|
|
88
|
+
synced = {"synced": [{"tool_id": "github:create_issue", "effect": "block"}]}
|
|
89
|
+
v = _client()._resolve("mcp_github_create_issue", {}, overrides, synced)
|
|
90
|
+
assert v.action == "block"
|
|
91
|
+
assert v.risk == "synced"
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"""Canonical tool-id normalization — the ONLY framework-specific mapping.
|
|
2
|
-
|
|
3
|
-
The whole SecureVector fleet keys permissions and audit on a single canonical
|
|
4
|
-
``tool_id``. CrewAI surfaces a tool's name as ``BaseTool.name`` (passed here as
|
|
5
|
-
the flat ``name`` argument). Normalizing it here is the one piece of
|
|
6
|
-
CrewAI-specific code; every downstream step (permission resolution, analysis,
|
|
7
|
-
audit) is shared engine behaviour, so a policy authored once — "allow
|
|
8
|
-
web_search, block shell" — applies identically across LangChain / LangGraph /
|
|
9
|
-
CrewAI.
|
|
10
|
-
|
|
11
|
-
Casing is preserved: the local app matches tool ids case-insensitively, so a
|
|
12
|
-
rule authored ``tool_id="Bash"`` still governs a CrewAI tool named ``bash``.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from typing import Any, Optional
|
|
16
|
-
|
|
17
|
-
# The audit/Bill-of-Tools/OCSF pipeline groups by this attribution tag.
|
|
18
|
-
RUNTIME_KIND = "crewai"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def normalize_tool_id(serialized: Any, name: Optional[str] = None) -> str:
|
|
22
|
-
"""Resolve a canonical tool id.
|
|
23
|
-
|
|
24
|
-
Accepts a flat ``name`` (CrewAI ``BaseTool.name``) and/or a ``serialized``
|
|
25
|
-
dict (``{"name": ...}`` or a dotted ``{"id": [...]}`` path) for symmetry
|
|
26
|
-
with the other SDKs. Falls back to ``"unknown"`` so a missing name never
|
|
27
|
-
crashes the agent.
|
|
28
|
-
"""
|
|
29
|
-
raw: Optional[str] = None
|
|
30
|
-
if isinstance(serialized, dict):
|
|
31
|
-
raw = serialized.get("name")
|
|
32
|
-
if not raw:
|
|
33
|
-
ident = serialized.get("id")
|
|
34
|
-
if isinstance(ident, (list, tuple)) and ident:
|
|
35
|
-
raw = str(ident[-1])
|
|
36
|
-
if not raw:
|
|
37
|
-
raw = name
|
|
38
|
-
if not raw:
|
|
39
|
-
return "unknown"
|
|
40
|
-
return str(raw).strip() or "unknown"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/auto.py
RENAMED
|
File without changes
|
{securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/core.py
RENAMED
|
File without changes
|
{securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/src/securevector_sdk_crewai/costs.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{securevector_sdk_crewai-1.2.1 → securevector_sdk_crewai-1.2.2}/tests/test_engine_endpoint.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|