open-edison 0.1.11__py3-none-any.whl → 0.1.16__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.
- {open_edison-0.1.11.dist-info → open_edison-0.1.16.dist-info}/METADATA +65 -120
- open_edison-0.1.16.dist-info/RECORD +18 -0
- src/frontend_dist/assets/index-_NTxjOfh.js +51 -0
- src/frontend_dist/assets/index-h6k8aL6h.css +1 -0
- src/frontend_dist/index.html +2 -2
- src/middleware/data_access_tracker.py +217 -133
- src/middleware/session_tracking.py +8 -1
- src/server.py +188 -7
- src/telemetry.py +23 -1
- open_edison-0.1.11.dist-info/RECORD +0 -18
- src/frontend_dist/assets/index-BPaXg1vr.js +0 -51
- src/frontend_dist/assets/index-BVdkI6ig.css +0 -1
- {open_edison-0.1.11.dist-info → open_edison-0.1.16.dist-info}/WHEEL +0 -0
- {open_edison-0.1.11.dist-info → open_edison-0.1.16.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.11.dist-info → open_edison-0.1.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width: 640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width: 768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width: 1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width: 1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width: 1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.relative{position:relative}.m-0{margin:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-2{height:.5rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-\[580px\]{height:580px}.w-10{width:2.5rem}.w-2{width:.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.min-w-\[240px\]{min-width:240px}.max-w-\[1400px\]{max-width:1400px}.max-w-\[260px\]{max-width:260px}.border-collapse{border-collapse:collapse}.translate-x-1{--tw-translate-x: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-5{--tw-translate-x: 1.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-amber-400\/30{border-color:#fbbf244d}.border-app-accent{border-color:var(--accent)}.border-app-border{border-color:var(--border)}.border-blue-400\/30{border-color:#60a5fa4d}.border-rose-400\/30{border-color:#fb71854d}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-app-accent{background-color:var(--accent)}.bg-app-bg{background-color:var(--bg)}.bg-app-border{background-color:var(--border)}.bg-blue-400{--tw-bg-opacity: 1;background-color:rgb(96 165 250 / var(--tw-bg-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-rose-400{--tw-bg-opacity: 1;background-color:rgb(251 113 133 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-6{padding:1.5rem}.\!px-3{padding-left:.75rem!important;padding-right:.75rem!important}.\!py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-2{padding-bottom:.5rem}.text-left{text-align:left}.text-center{text-align:center}.align-bottom{vertical-align:bottom}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-app-accent{color:var(--accent)}.text-app-muted{color:var(--muted)}.text-app-text{color:var(--text)}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-rose-400{--tw-text-opacity: 1;color:rgb(251 113 133 / var(--tw-text-opacity, 1))}.accent-blue-500{accent-color:#3b82f6}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{--bg: #0b0c10;--card: #111318;--border: #1f2430;--text: #e6e6e6;--muted: #a0a7b4;--accent: #7c3aed;--success: #10b981;--warning: #f59e0b;--danger: #ef4444}[data-theme=dark]{--bg: #0b0c10;--card: #111318;--border: #1f2430;--text: #e6e6e6;--muted: #a0a7b4}[data-theme=light]{--bg: #f8fafc;--card: #ffffff;--border: #e5e7eb;--text: #0f172a;--muted: #475569}@media (prefers-color-scheme: light){:root{--bg: #f8fafc;--card: #ffffff;--border: #e5e7eb;--text: #0f172a;--muted: #475569}}html,body,#root{height:100%}body{margin:0;background:var(--bg);color:var(--text)}.container{margin:0 auto;padding:24px;max-width:1100px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px;box-shadow:0 1px 2px #0000000a,0 2px 12px #00000014}.stat{display:flex;align-items:center;gap:12px}.badge{display:inline-block;font-size:12px;padding:2px 8px;border-radius:999px;border:1px solid var(--border);background:#7c3aed14;color:var(--text)}.table{width:100%;border-collapse:collapse}.table th,.table td{border-bottom:1px solid var(--border);padding:8px 4px;text-align:left}.muted{color:var(--muted)}.accent{color:var(--accent)}.success{color:var(--success)}.warning{color:var(--warning)}.danger{color:var(--danger)}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px}.button{border:1px solid var(--border);background:var(--card);color:var(--text);padding:6px 10px;border-radius:8px;cursor:pointer}.button:hover{filter:brightness(1.05)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-blue-400:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(96 165 250 / var(--tw-ring-opacity, 1))}@media (min-width: 640px){.sm\:flex{display:flex}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-end{align-items:flex-end}}@media (min-width: 1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-\[220px_1fr\]{grid-template-columns:220px 1fr}}
|
src/frontend_dist/index.html
CHANGED
@@ -10,8 +10,8 @@
|
|
10
10
|
const prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
11
11
|
document.documentElement.setAttribute('data-theme', prefersLight ? 'light' : 'dark');
|
12
12
|
</script>
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
13
|
+
<script type="module" crossorigin src="/assets/index-_NTxjOfh.js"></script>
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-h6k8aL6h.css">
|
15
15
|
</head>
|
16
16
|
|
17
17
|
<body>
|
@@ -14,7 +14,7 @@ names (with server-name/path prefixes) to their security classifications:
|
|
14
14
|
|
15
15
|
import json
|
16
16
|
from dataclasses import dataclass
|
17
|
-
from functools import
|
17
|
+
from functools import cache
|
18
18
|
from pathlib import Path
|
19
19
|
from typing import Any
|
20
20
|
|
@@ -23,20 +23,77 @@ from loguru import logger as log
|
|
23
23
|
from src.config import ConfigError
|
24
24
|
from src.telemetry import (
|
25
25
|
record_private_data_access,
|
26
|
+
record_prompt_access_blocked,
|
27
|
+
record_resource_access_blocked,
|
26
28
|
record_tool_call_blocked,
|
27
29
|
record_untrusted_public_data,
|
28
30
|
record_write_operation,
|
29
31
|
)
|
30
32
|
|
33
|
+
ACL_RANK: dict[str, int] = {"PUBLIC": 0, "PRIVATE": 1, "SECRET": 2}
|
31
34
|
|
32
|
-
|
35
|
+
# Default flat permissions applied when fields are missing in config
|
36
|
+
DEFAULT_PERMISSIONS: dict[str, Any] = {
|
37
|
+
"enabled": False,
|
38
|
+
"write_operation": False,
|
39
|
+
"read_private_data": False,
|
40
|
+
"read_untrusted_public_data": False,
|
41
|
+
"acl": "PUBLIC",
|
42
|
+
}
|
43
|
+
|
44
|
+
|
45
|
+
def _normalize_acl(value: Any, *, default: str = "PUBLIC") -> str:
|
46
|
+
"""Normalize ACL string, defaulting and uppercasing; validate against known values."""
|
47
|
+
try:
|
48
|
+
if value is None:
|
49
|
+
return default
|
50
|
+
acl = str(value).upper().strip()
|
51
|
+
if acl not in ACL_RANK:
|
52
|
+
# Fallback to default if invalid
|
53
|
+
return default
|
54
|
+
return acl
|
55
|
+
except Exception:
|
56
|
+
return default
|
57
|
+
|
58
|
+
|
59
|
+
def _apply_permission_defaults(config_perms: dict[str, Any]) -> dict[str, Any]:
|
60
|
+
"""Merge provided config flags with DEFAULT_PERMISSIONS, including ACL derivation."""
|
61
|
+
# Start from defaults
|
62
|
+
merged: dict[str, Any] = dict(DEFAULT_PERMISSIONS)
|
63
|
+
# Booleans
|
64
|
+
enabled = bool(config_perms.get("enabled", merged["enabled"]))
|
65
|
+
write_operation = bool(config_perms.get("write_operation", merged["write_operation"]))
|
66
|
+
read_private_data = bool(config_perms.get("read_private_data", merged["read_private_data"]))
|
67
|
+
read_untrusted_public_data = bool(
|
68
|
+
config_perms.get("read_untrusted_public_data", merged["read_untrusted_public_data"]) # type: ignore[reportUnknownArgumentType]
|
69
|
+
)
|
70
|
+
|
71
|
+
# ACL: explicit value wins; otherwise default PRIVATE if read_private_data True, else default
|
72
|
+
if "acl" in config_perms and config_perms.get("acl") is not None:
|
73
|
+
acl = _normalize_acl(config_perms.get("acl"), default=str(merged["acl"]))
|
74
|
+
else:
|
75
|
+
acl = _normalize_acl("PRIVATE" if read_private_data else str(merged["acl"]))
|
76
|
+
|
77
|
+
merged.update(
|
78
|
+
{
|
79
|
+
"enabled": enabled,
|
80
|
+
"write_operation": write_operation,
|
81
|
+
"read_private_data": read_private_data,
|
82
|
+
"read_untrusted_public_data": read_untrusted_public_data,
|
83
|
+
"acl": acl,
|
84
|
+
}
|
85
|
+
)
|
86
|
+
return merged
|
87
|
+
|
88
|
+
|
89
|
+
def _flat_permissions_loader(config_path: Path) -> dict[str, dict[str, Any]]:
|
33
90
|
if config_path.exists():
|
34
91
|
with open(config_path) as f:
|
35
92
|
data: dict[str, Any] = json.load(f)
|
36
93
|
|
37
94
|
# Handle new format: server -> {tool -> permissions}
|
38
95
|
# Convert to flat tool -> permissions format
|
39
|
-
flat_permissions: dict[str, dict[str,
|
96
|
+
flat_permissions: dict[str, dict[str, Any]] = {}
|
40
97
|
tool_to_server: dict[str, str] = {}
|
41
98
|
server_tools: dict[str, set[str]] = {}
|
42
99
|
|
@@ -83,14 +140,7 @@ def _flat_permissions_loader(config_path: Path) -> dict[str, dict[str, bool]]:
|
|
83
140
|
|
84
141
|
# Convert to flat format with explicit type casting
|
85
142
|
tool_perms_dict: dict[str, Any] = tool_permissions # type: ignore
|
86
|
-
flat_permissions[tool_name] =
|
87
|
-
"enabled": bool(tool_perms_dict.get("enabled", True)),
|
88
|
-
"write_operation": bool(tool_perms_dict.get("write_operation", False)),
|
89
|
-
"read_private_data": bool(tool_perms_dict.get("read_private_data", False)),
|
90
|
-
"read_untrusted_public_data": bool(
|
91
|
-
tool_perms_dict.get("read_untrusted_public_data", False)
|
92
|
-
),
|
93
|
-
}
|
143
|
+
flat_permissions[tool_name] = _apply_permission_defaults(tool_perms_dict)
|
94
144
|
|
95
145
|
log.debug(
|
96
146
|
f"Loaded {len(flat_permissions)} tool permissions from {len(server_tools)} servers in {config_path}"
|
@@ -106,8 +156,8 @@ def _flat_permissions_loader(config_path: Path) -> dict[str, dict[str, bool]]:
|
|
106
156
|
return {}
|
107
157
|
|
108
158
|
|
109
|
-
@
|
110
|
-
def _load_tool_permissions_cached() -> dict[str, dict[str,
|
159
|
+
@cache
|
160
|
+
def _load_tool_permissions_cached() -> dict[str, dict[str, Any]]:
|
111
161
|
"""Load tool permissions from JSON configuration file with LRU caching."""
|
112
162
|
config_path = Path(__file__).parent.parent.parent / "tool_permissions.json"
|
113
163
|
|
@@ -121,8 +171,8 @@ def _load_tool_permissions_cached() -> dict[str, dict[str, bool]]:
|
|
121
171
|
return {}
|
122
172
|
|
123
173
|
|
124
|
-
@
|
125
|
-
def _load_resource_permissions_cached() -> dict[str, dict[str,
|
174
|
+
@cache
|
175
|
+
def _load_resource_permissions_cached() -> dict[str, dict[str, Any]]:
|
126
176
|
"""Load resource permissions from JSON configuration file with LRU caching."""
|
127
177
|
config_path = Path(__file__).parent.parent.parent / "resource_permissions.json"
|
128
178
|
|
@@ -136,8 +186,8 @@ def _load_resource_permissions_cached() -> dict[str, dict[str, bool]]:
|
|
136
186
|
return {}
|
137
187
|
|
138
188
|
|
139
|
-
@
|
140
|
-
def _load_prompt_permissions_cached() -> dict[str, dict[str,
|
189
|
+
@cache
|
190
|
+
def _load_prompt_permissions_cached() -> dict[str, dict[str, Any]]:
|
141
191
|
"""Load prompt permissions from JSON configuration file with LRU caching."""
|
142
192
|
config_path = Path(__file__).parent.parent.parent / "prompt_permissions.json"
|
143
193
|
|
@@ -151,55 +201,55 @@ def _load_prompt_permissions_cached() -> dict[str, dict[str, bool]]:
|
|
151
201
|
return {}
|
152
202
|
|
153
203
|
|
154
|
-
@
|
155
|
-
def _classify_tool_permissions_cached(tool_name: str) -> dict[str,
|
204
|
+
@cache
|
205
|
+
def _classify_tool_permissions_cached(tool_name: str) -> dict[str, Any]:
|
156
206
|
"""Classify tool permissions with LRU caching."""
|
157
207
|
return _classify_permissions_cached(tool_name, _load_tool_permissions_cached(), "tool")
|
158
208
|
|
159
209
|
|
160
|
-
@
|
161
|
-
def _classify_resource_permissions_cached(resource_name: str) -> dict[str,
|
210
|
+
@cache
|
211
|
+
def _classify_resource_permissions_cached(resource_name: str) -> dict[str, Any]:
|
162
212
|
"""Classify resource permissions with LRU caching."""
|
163
213
|
return _classify_permissions_cached(
|
164
214
|
resource_name, _load_resource_permissions_cached(), "resource"
|
165
215
|
)
|
166
216
|
|
167
217
|
|
168
|
-
@
|
169
|
-
def _classify_prompt_permissions_cached(prompt_name: str) -> dict[str,
|
218
|
+
@cache
|
219
|
+
def _classify_prompt_permissions_cached(prompt_name: str) -> dict[str, Any]:
|
170
220
|
"""Classify prompt permissions with LRU caching."""
|
171
221
|
return _classify_permissions_cached(prompt_name, _load_prompt_permissions_cached(), "prompt")
|
172
222
|
|
173
223
|
|
174
|
-
def _get_builtin_tool_permissions(name: str) -> dict[str,
|
224
|
+
def _get_builtin_tool_permissions(name: str) -> dict[str, Any] | None:
|
175
225
|
"""Get permissions for built-in safe tools."""
|
176
226
|
builtin_safe_tools = ["echo", "get_server_info", "get_security_status"]
|
177
227
|
if name in builtin_safe_tools:
|
178
|
-
permissions = {
|
179
|
-
"enabled": True,
|
180
|
-
"write_operation": False,
|
181
|
-
"read_private_data": False,
|
182
|
-
"read_untrusted_public_data": False,
|
183
|
-
}
|
228
|
+
permissions = _apply_permission_defaults({"enabled": True})
|
184
229
|
log.debug(f"Built-in safe tool {name}: {permissions}")
|
185
230
|
return permissions
|
186
231
|
return None
|
187
232
|
|
188
233
|
|
189
234
|
def _get_exact_match_permissions(
|
190
|
-
name: str, permissions_config: dict[str, dict[str,
|
191
|
-
) -> dict[str,
|
235
|
+
name: str, permissions_config: dict[str, dict[str, Any]], type_name: str
|
236
|
+
) -> dict[str, Any] | None:
|
192
237
|
"""Check for exact match permissions."""
|
193
238
|
if name in permissions_config and not name.startswith("_"):
|
194
239
|
config_perms = permissions_config[name]
|
195
|
-
permissions =
|
196
|
-
"enabled": config_perms.get("enabled", False),
|
197
|
-
"write_operation": config_perms.get("write_operation", False),
|
198
|
-
"read_private_data": config_perms.get("read_private_data", False),
|
199
|
-
"read_untrusted_public_data": config_perms.get("read_untrusted_public_data", False),
|
200
|
-
}
|
240
|
+
permissions = _apply_permission_defaults(config_perms)
|
201
241
|
log.debug(f"Found exact match for {type_name} {name}: {permissions}")
|
202
242
|
return permissions
|
243
|
+
# Fallback: support names like "server_tool" by checking the part after first underscore
|
244
|
+
if "_" in name:
|
245
|
+
suffix = name.split("_", 1)[1]
|
246
|
+
if suffix in permissions_config and not suffix.startswith("_"):
|
247
|
+
config_perms = permissions_config[suffix]
|
248
|
+
permissions = _apply_permission_defaults(config_perms)
|
249
|
+
log.debug(
|
250
|
+
f"Found fallback match for {type_name} {name} using suffix {suffix}: {permissions}"
|
251
|
+
)
|
252
|
+
return permissions
|
203
253
|
return None
|
204
254
|
|
205
255
|
|
@@ -230,8 +280,8 @@ def _get_wildcard_patterns(name: str, type_name: str) -> list[str]:
|
|
230
280
|
|
231
281
|
|
232
282
|
def _classify_permissions_cached(
|
233
|
-
name: str, permissions_config: dict[str, dict[str,
|
234
|
-
) -> dict[str,
|
283
|
+
name: str, permissions_config: dict[str, dict[str, Any]], type_name: str
|
284
|
+
) -> dict[str, Any]:
|
235
285
|
"""Generic permission classification with pattern matching support."""
|
236
286
|
# Built-in safe tools that don't need external config (only for tools)
|
237
287
|
if type_name == "tool":
|
@@ -249,12 +299,7 @@ def _classify_permissions_cached(
|
|
249
299
|
for pattern in wildcard_patterns:
|
250
300
|
if pattern in permissions_config:
|
251
301
|
config_perms = permissions_config[pattern]
|
252
|
-
permissions =
|
253
|
-
"enabled": config_perms.get("enabled", False),
|
254
|
-
"write_operation": config_perms.get("write_operation", False),
|
255
|
-
"read_private_data": config_perms.get("read_private_data", False),
|
256
|
-
"read_untrusted_public_data": config_perms.get("read_untrusted_public_data", False),
|
257
|
-
}
|
302
|
+
permissions = _apply_permission_defaults(config_perms)
|
258
303
|
log.debug(f"Found wildcard match for {type_name} {name} using {pattern}: {permissions}")
|
259
304
|
return permissions
|
260
305
|
|
@@ -283,6 +328,8 @@ class DataAccessTracker:
|
|
283
328
|
has_private_data_access: bool = False
|
284
329
|
has_untrusted_content_exposure: bool = False
|
285
330
|
has_external_communication: bool = False
|
331
|
+
# ACL tracking: the most restrictive ACL encountered during this session via reads
|
332
|
+
highest_acl_level: str = "PUBLIC"
|
286
333
|
|
287
334
|
def is_trifecta_achieved(self) -> bool:
|
288
335
|
"""Check if the lethal trifecta has been achieved."""
|
@@ -292,31 +339,31 @@ class DataAccessTracker:
|
|
292
339
|
and self.has_external_communication
|
293
340
|
)
|
294
341
|
|
295
|
-
def _load_tool_permissions(self) -> dict[str, dict[str,
|
342
|
+
def _load_tool_permissions(self) -> dict[str, dict[str, Any]]:
|
296
343
|
"""Load tool permissions from JSON configuration file with caching."""
|
297
344
|
return _load_tool_permissions_cached()
|
298
345
|
|
299
|
-
def _load_resource_permissions(self) -> dict[str, dict[str,
|
346
|
+
def _load_resource_permissions(self) -> dict[str, dict[str, Any]]:
|
300
347
|
"""Load resource permissions from JSON configuration file with caching."""
|
301
348
|
return _load_resource_permissions_cached()
|
302
349
|
|
303
|
-
def _load_prompt_permissions(self) -> dict[str, dict[str,
|
350
|
+
def _load_prompt_permissions(self) -> dict[str, dict[str, Any]]:
|
304
351
|
"""Load prompt permissions from JSON configuration file with caching."""
|
305
352
|
return _load_prompt_permissions_cached()
|
306
353
|
|
307
|
-
def _classify_by_tool_name(self, tool_name: str) -> dict[str,
|
354
|
+
def _classify_by_tool_name(self, tool_name: str) -> dict[str, Any]:
|
308
355
|
"""Classify permissions based on external JSON configuration only."""
|
309
356
|
return _classify_tool_permissions_cached(tool_name)
|
310
357
|
|
311
|
-
def _classify_by_resource_name(self, resource_name: str) -> dict[str,
|
358
|
+
def _classify_by_resource_name(self, resource_name: str) -> dict[str, Any]:
|
312
359
|
"""Classify resource permissions based on external JSON configuration only."""
|
313
360
|
return _classify_resource_permissions_cached(resource_name)
|
314
361
|
|
315
|
-
def _classify_by_prompt_name(self, prompt_name: str) -> dict[str,
|
362
|
+
def _classify_by_prompt_name(self, prompt_name: str) -> dict[str, Any]:
|
316
363
|
"""Classify prompt permissions based on external JSON configuration only."""
|
317
364
|
return _classify_prompt_permissions_cached(prompt_name)
|
318
365
|
|
319
|
-
def _classify_tool_permissions(self, tool_name: str) -> dict[str,
|
366
|
+
def _classify_tool_permissions(self, tool_name: str) -> dict[str, Any]:
|
320
367
|
"""
|
321
368
|
Classify tool permissions based on tool name.
|
322
369
|
|
@@ -329,7 +376,7 @@ class DataAccessTracker:
|
|
329
376
|
log.debug(f"Classified tool {tool_name}: {permissions}")
|
330
377
|
return permissions
|
331
378
|
|
332
|
-
def _classify_resource_permissions(self, resource_name: str) -> dict[str,
|
379
|
+
def _classify_resource_permissions(self, resource_name: str) -> dict[str, Any]:
|
333
380
|
"""
|
334
381
|
Classify resource permissions based on resource name.
|
335
382
|
|
@@ -342,7 +389,7 @@ class DataAccessTracker:
|
|
342
389
|
log.debug(f"Classified resource {resource_name}: {permissions}")
|
343
390
|
return permissions
|
344
391
|
|
345
|
-
def _classify_prompt_permissions(self, prompt_name: str) -> dict[str,
|
392
|
+
def _classify_prompt_permissions(self, prompt_name: str) -> dict[str, Any]:
|
346
393
|
"""
|
347
394
|
Classify prompt permissions based on prompt name.
|
348
395
|
|
@@ -355,36 +402,90 @@ class DataAccessTracker:
|
|
355
402
|
log.debug(f"Classified prompt {prompt_name}: {permissions}")
|
356
403
|
return permissions
|
357
404
|
|
358
|
-
def get_tool_permissions(self, tool_name: str) -> dict[str,
|
405
|
+
def get_tool_permissions(self, tool_name: str) -> dict[str, Any]:
|
359
406
|
"""Get tool permissions based on tool name."""
|
360
407
|
return self._classify_tool_permissions(tool_name)
|
361
408
|
|
362
|
-
def get_resource_permissions(self, resource_name: str) -> dict[str,
|
409
|
+
def get_resource_permissions(self, resource_name: str) -> dict[str, Any]:
|
363
410
|
"""Get resource permissions based on resource name."""
|
364
411
|
return self._classify_resource_permissions(resource_name)
|
365
412
|
|
366
|
-
def get_prompt_permissions(self, prompt_name: str) -> dict[str,
|
413
|
+
def get_prompt_permissions(self, prompt_name: str) -> dict[str, Any]:
|
367
414
|
"""Get prompt permissions based on prompt name."""
|
368
415
|
return self._classify_prompt_permissions(prompt_name)
|
369
416
|
|
370
|
-
def
|
417
|
+
def _would_call_complete_trifecta(self, permissions: dict[str, Any]) -> bool:
|
418
|
+
"""Return True if applying these permissions would complete the trifecta."""
|
419
|
+
would_private = self.has_private_data_access or bool(permissions.get("read_private_data"))
|
420
|
+
would_untrusted = self.has_untrusted_content_exposure or bool(
|
421
|
+
permissions.get("read_untrusted_public_data")
|
422
|
+
)
|
423
|
+
would_write = self.has_external_communication or bool(permissions.get("write_operation"))
|
424
|
+
return bool(would_private and would_untrusted and would_write)
|
425
|
+
|
426
|
+
def _enforce_tool_enabled(self, permissions: dict[str, Any], tool_name: str) -> None:
|
427
|
+
if permissions["enabled"] is False:
|
428
|
+
log.warning(f"🚫 BLOCKING tool call {tool_name} - tool is disabled")
|
429
|
+
record_tool_call_blocked(tool_name, "disabled")
|
430
|
+
raise SecurityError(f"'{tool_name}' / Tool disabled")
|
431
|
+
|
432
|
+
def _enforce_acl_downgrade_block(
|
433
|
+
self, tool_acl: str, permissions: dict[str, Any], tool_name: str
|
434
|
+
) -> None:
|
435
|
+
if permissions["write_operation"]:
|
436
|
+
current_rank = ACL_RANK.get(self.highest_acl_level, 0)
|
437
|
+
write_rank = ACL_RANK.get(tool_acl, 0)
|
438
|
+
if write_rank < current_rank:
|
439
|
+
log.error(
|
440
|
+
f"🚫 BLOCKING tool call {tool_name} - write to lower ACL ({tool_acl}) while session has higher ACL {self.highest_acl_level}"
|
441
|
+
)
|
442
|
+
record_tool_call_blocked(tool_name, "acl_downgrade")
|
443
|
+
raise SecurityError(f"'{tool_name}' / ACL (level={self.highest_acl_level})")
|
444
|
+
|
445
|
+
def _apply_permissions_effects(
|
446
|
+
self,
|
447
|
+
permissions: dict[str, Any],
|
448
|
+
*,
|
449
|
+
source_type: str,
|
450
|
+
name: str,
|
451
|
+
) -> None:
|
452
|
+
"""Apply side effects (flags, ACL, telemetry) for any source type."""
|
453
|
+
acl_value: str = _normalize_acl(permissions.get("acl"), default="PUBLIC")
|
454
|
+
if permissions["read_private_data"]:
|
455
|
+
self.has_private_data_access = True
|
456
|
+
log.info(f"🔒 Private data access detected via {source_type}: {name}")
|
457
|
+
record_private_data_access(source_type, name)
|
458
|
+
# Update highest ACL based on ACL when reading private data
|
459
|
+
current_rank = ACL_RANK.get(self.highest_acl_level, 0)
|
460
|
+
new_rank = ACL_RANK.get(acl_value, 0)
|
461
|
+
if new_rank > current_rank:
|
462
|
+
self.highest_acl_level = acl_value
|
463
|
+
|
464
|
+
if permissions["read_untrusted_public_data"]:
|
465
|
+
self.has_untrusted_content_exposure = True
|
466
|
+
log.info(f"🌐 Untrusted content exposure detected via {source_type}: {name}")
|
467
|
+
record_untrusted_public_data(source_type, name)
|
468
|
+
|
469
|
+
if permissions["write_operation"]:
|
470
|
+
self.has_external_communication = True
|
471
|
+
log.info(f"✍️ Write operation detected via {source_type}: {name}")
|
472
|
+
record_write_operation(source_type, name)
|
473
|
+
|
474
|
+
def add_tool_call(self, tool_name: str):
|
371
475
|
"""
|
372
476
|
Add a tool call and update trifecta flags based on tool classification.
|
373
477
|
|
374
478
|
Args:
|
375
479
|
tool_name: Name of the tool being called
|
376
480
|
|
377
|
-
Returns:
|
378
|
-
Placeholder ID for compatibility
|
379
|
-
|
380
481
|
Raises:
|
381
482
|
SecurityError: If the lethal trifecta is already achieved and this call would be blocked
|
382
483
|
"""
|
383
484
|
# Check if trifecta is already achieved before processing this call
|
384
485
|
if self.is_trifecta_achieved():
|
385
|
-
log.error(f"🚫 BLOCKING tool call {tool_name} - lethal trifecta
|
486
|
+
log.error(f"🚫 BLOCKING tool call {tool_name} - lethal trifecta achieved")
|
386
487
|
record_tool_call_blocked(tool_name, "trifecta")
|
387
|
-
raise SecurityError(f"
|
488
|
+
raise SecurityError(f"'{tool_name}' / Lethal trifecta")
|
388
489
|
|
389
490
|
# Get tool permissions and update trifecta flags
|
390
491
|
permissions = self._classify_tool_permissions(tool_name)
|
@@ -392,42 +493,30 @@ class DataAccessTracker:
|
|
392
493
|
log.debug(f"add_tool_call: Tool permissions: {permissions}")
|
393
494
|
|
394
495
|
# Check if tool is enabled
|
395
|
-
|
396
|
-
log.warning(f"🚫 BLOCKING tool call {tool_name} - tool is disabled")
|
397
|
-
record_tool_call_blocked(tool_name, "disabled")
|
398
|
-
raise SecurityError(f"Tool call '{tool_name}' blocked: tool is disabled")
|
496
|
+
self._enforce_tool_enabled(permissions, tool_name)
|
399
497
|
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
record_private_data_access("tool", tool_name)
|
498
|
+
# ACL-based write downgrade prevention
|
499
|
+
tool_acl: str = _normalize_acl(permissions.get("acl"), default="PUBLIC")
|
500
|
+
self._enforce_acl_downgrade_block(tool_acl, permissions, tool_name)
|
404
501
|
|
405
|
-
|
406
|
-
|
407
|
-
log.
|
408
|
-
|
502
|
+
# Pre-check: would this call achieve the lethal trifecta? If so, block immediately
|
503
|
+
if self._would_call_complete_trifecta(permissions):
|
504
|
+
log.error(f"🚫 BLOCKING tool call {tool_name} - would achieve lethal trifecta")
|
505
|
+
record_tool_call_blocked(tool_name, "trifecta_prevent")
|
506
|
+
raise SecurityError(f"'{tool_name}' / Lethal trifecta")
|
409
507
|
|
410
|
-
|
411
|
-
self.has_external_communication = True
|
412
|
-
log.info(f"✍️ Write operation detected: {tool_name}")
|
413
|
-
record_write_operation("tool", tool_name)
|
414
|
-
|
415
|
-
# Log if trifecta is achieved after this call
|
416
|
-
if self.is_trifecta_achieved():
|
417
|
-
log.warning(f"⚠️ LETHAL TRIFECTA ACHIEVED after tool call: {tool_name}")
|
508
|
+
self._apply_permissions_effects(permissions, source_type="tool", name=tool_name)
|
418
509
|
|
419
|
-
|
510
|
+
# We proactively prevent trifecta; by design we should never reach a state where
|
511
|
+
# a completed call newly achieves trifecta.
|
420
512
|
|
421
|
-
def add_resource_access(self, resource_name: str)
|
513
|
+
def add_resource_access(self, resource_name: str):
|
422
514
|
"""
|
423
515
|
Add a resource access and update trifecta flags based on resource classification.
|
424
516
|
|
425
517
|
Args:
|
426
518
|
resource_name: Name/URI of the resource being accessed
|
427
519
|
|
428
|
-
Returns:
|
429
|
-
Placeholder ID for compatibility
|
430
|
-
|
431
520
|
Raises:
|
432
521
|
SecurityError: If the lethal trifecta is already achieved and this access would be blocked
|
433
522
|
"""
|
@@ -436,75 +525,52 @@ class DataAccessTracker:
|
|
436
525
|
log.error(
|
437
526
|
f"🚫 BLOCKING resource access {resource_name} - lethal trifecta already achieved"
|
438
527
|
)
|
439
|
-
raise SecurityError(
|
440
|
-
f"Resource access '{resource_name}' blocked: lethal trifecta achieved"
|
441
|
-
)
|
528
|
+
raise SecurityError(f"'{resource_name}' / Lethal trifecta")
|
442
529
|
|
443
530
|
# Get resource permissions and update trifecta flags
|
444
531
|
permissions = self._classify_resource_permissions(resource_name)
|
445
532
|
|
446
|
-
|
447
|
-
|
448
|
-
log.
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
log.info(f"🌐 Untrusted content exposure detected via resource: {resource_name}")
|
454
|
-
record_untrusted_public_data("resource", resource_name)
|
455
|
-
|
456
|
-
if permissions["write_operation"]:
|
457
|
-
self.has_external_communication = True
|
458
|
-
log.info(f"✍️ Write operation detected via resource: {resource_name}")
|
459
|
-
record_write_operation("resource", resource_name)
|
533
|
+
# Pre-check: would this access achieve the lethal trifecta? If so, block immediately
|
534
|
+
if self._would_call_complete_trifecta(permissions):
|
535
|
+
log.error(
|
536
|
+
f"🚫 BLOCKING resource access {resource_name} - would achieve lethal trifecta"
|
537
|
+
)
|
538
|
+
record_resource_access_blocked(resource_name, "trifecta_prevent")
|
539
|
+
raise SecurityError(f"'{resource_name}' / Lethal trifecta")
|
460
540
|
|
461
|
-
|
462
|
-
if self.is_trifecta_achieved():
|
463
|
-
log.warning(f"⚠️ LETHAL TRIFECTA ACHIEVED after resource access: {resource_name}")
|
541
|
+
self._apply_permissions_effects(permissions, source_type="resource", name=resource_name)
|
464
542
|
|
465
|
-
|
543
|
+
# We proactively prevent trifecta; by design we should never reach a state where
|
544
|
+
# a completed access newly achieves trifecta.
|
466
545
|
|
467
|
-
def add_prompt_access(self, prompt_name: str)
|
546
|
+
def add_prompt_access(self, prompt_name: str):
|
468
547
|
"""
|
469
548
|
Add a prompt access and update trifecta flags based on prompt classification.
|
470
549
|
|
471
550
|
Args:
|
472
551
|
prompt_name: Name/type of the prompt being accessed
|
473
552
|
|
474
|
-
Returns:
|
475
|
-
Placeholder ID for compatibility
|
476
|
-
|
477
553
|
Raises:
|
478
554
|
SecurityError: If the lethal trifecta is already achieved and this access would be blocked
|
479
555
|
"""
|
480
556
|
# Check if trifecta is already achieved before processing this access
|
481
557
|
if self.is_trifecta_achieved():
|
482
558
|
log.error(f"🚫 BLOCKING prompt access {prompt_name} - lethal trifecta already achieved")
|
483
|
-
raise SecurityError(f"
|
559
|
+
raise SecurityError(f"'{prompt_name}' / Lethal trifecta")
|
484
560
|
|
485
561
|
# Get prompt permissions and update trifecta flags
|
486
562
|
permissions = self._classify_prompt_permissions(prompt_name)
|
487
563
|
|
488
|
-
|
489
|
-
|
490
|
-
log.
|
491
|
-
|
492
|
-
|
493
|
-
if permissions["read_untrusted_public_data"]:
|
494
|
-
self.has_untrusted_content_exposure = True
|
495
|
-
log.info(f"🌐 Untrusted content exposure detected via prompt: {prompt_name}")
|
496
|
-
record_untrusted_public_data("prompt", prompt_name)
|
564
|
+
# Pre-check: would this access achieve the lethal trifecta? If so, block immediately
|
565
|
+
if self._would_call_complete_trifecta(permissions):
|
566
|
+
log.error(f"🚫 BLOCKING prompt access {prompt_name} - would achieve lethal trifecta")
|
567
|
+
record_prompt_access_blocked(prompt_name, "trifecta_prevent")
|
568
|
+
raise SecurityError(f"'{prompt_name}' / Lethal trifecta")
|
497
569
|
|
498
|
-
|
499
|
-
self.has_external_communication = True
|
500
|
-
log.info(f"✍️ Write operation detected via prompt: {prompt_name}")
|
501
|
-
record_write_operation("prompt", prompt_name)
|
570
|
+
self._apply_permissions_effects(permissions, source_type="prompt", name=prompt_name)
|
502
571
|
|
503
|
-
#
|
504
|
-
|
505
|
-
log.warning(f"⚠️ LETHAL TRIFECTA ACHIEVED after prompt access: {prompt_name}")
|
506
|
-
|
507
|
-
return "placeholder_id"
|
572
|
+
# We proactively prevent trifecta; by design we should never reach a state where
|
573
|
+
# a completed access newly achieves trifecta.
|
508
574
|
|
509
575
|
def to_dict(self) -> dict[str, Any]:
|
510
576
|
"""
|
@@ -520,8 +586,26 @@ class DataAccessTracker:
|
|
520
586
|
"has_external_communication": self.has_external_communication,
|
521
587
|
"trifecta_achieved": self.is_trifecta_achieved(),
|
522
588
|
},
|
589
|
+
"acl": {
|
590
|
+
"highest_acl_level": self.highest_acl_level,
|
591
|
+
},
|
523
592
|
}
|
524
593
|
|
525
594
|
|
526
595
|
class SecurityError(Exception):
|
527
596
|
"""Raised when a security policy violation occurs."""
|
597
|
+
|
598
|
+
def __init__(self, message: str):
|
599
|
+
"""We format with a brick ascii wall"""
|
600
|
+
message = f"""
|
601
|
+
████ ████ ████ ████ ████ ████
|
602
|
+
██ ████ ████ ████ ████ ████ █
|
603
|
+
████ ████ ████ ████ ████ ████
|
604
|
+
BLOCKED BY EDISON
|
605
|
+
{message:^30}
|
606
|
+
████ ████ ████ ████ ████ ████
|
607
|
+
██ ████ ████ ████ ████ ████ █
|
608
|
+
████ ████ ████ ████ ████ ████
|
609
|
+
{message}
|
610
|
+
"""
|
611
|
+
super().__init__(message)
|
@@ -170,6 +170,13 @@ def get_session_from_db(session_id: str) -> MCPSession:
|
|
170
170
|
data_access_tracker.has_external_communication = trifecta.get(
|
171
171
|
"has_external_communication", False
|
172
172
|
)
|
173
|
+
# Restore ACL highest level if present
|
174
|
+
if isinstance(summary_data, dict) and "acl" in summary_data:
|
175
|
+
acl_summary: Any = summary_data.get("acl") # type: ignore
|
176
|
+
if isinstance(acl_summary, dict):
|
177
|
+
highest = acl_summary.get("highest_acl_level") # type: ignore
|
178
|
+
if isinstance(highest, str) and highest:
|
179
|
+
data_access_tracker.highest_acl_level = highest
|
173
180
|
|
174
181
|
return MCPSession(
|
175
182
|
session_id=session_id,
|
@@ -289,7 +296,7 @@ class SessionTrackingMiddleware(Middleware):
|
|
289
296
|
|
290
297
|
assert session.data_access_tracker is not None
|
291
298
|
log.debug(f"🔍 Analyzing tool {context.message.name} for security implications")
|
292
|
-
|
299
|
+
session.data_access_tracker.add_tool_call(context.message.name)
|
293
300
|
# Telemetry: record tool call
|
294
301
|
record_tool_call(context.message.name)
|
295
302
|
|