open-edison 0.1.10__py3-none-any.whl → 0.1.11__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.10.dist-info → open_edison-0.1.11.dist-info}/METADATA +4 -1
- open_edison-0.1.11.dist-info/RECORD +18 -0
- src/cli.py +2 -2
- src/config.py +53 -1
- src/frontend_dist/assets/index-BPaXg1vr.js +51 -0
- src/frontend_dist/assets/index-BVdkI6ig.css +1 -0
- src/frontend_dist/index.html +2 -2
- src/middleware/data_access_tracker.py +17 -0
- src/middleware/session_tracking.py +9 -0
- src/server.py +7 -0
- src/telemetry.py +314 -0
- open_edison-0.1.10.dist-info/RECORD +0 -17
- src/frontend_dist/assets/index-CKkid2y-.js +0 -51
- src/frontend_dist/assets/index-CRxojymD.css +0 -1
- {open_edison-0.1.10.dist-info → open_edison-0.1.11.dist-info}/WHEEL +0 -0
- {open_edison-0.1.10.dist-info → open_edison-0.1.11.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.10.dist-info → open_edison-0.1.11.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}.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-full{width:100%!important}.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-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!important}.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\:grid-cols-3{grid-template-columns:repeat(3,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-BPaXg1vr.js"></script>
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BVdkI6ig.css">
|
15
15
|
</head>
|
16
16
|
|
17
17
|
<body>
|
@@ -21,6 +21,12 @@ from typing import Any
|
|
21
21
|
from loguru import logger as log
|
22
22
|
|
23
23
|
from src.config import ConfigError
|
24
|
+
from src.telemetry import (
|
25
|
+
record_private_data_access,
|
26
|
+
record_tool_call_blocked,
|
27
|
+
record_untrusted_public_data,
|
28
|
+
record_write_operation,
|
29
|
+
)
|
24
30
|
|
25
31
|
|
26
32
|
def _flat_permissions_loader(config_path: Path) -> dict[str, dict[str, bool]]:
|
@@ -377,6 +383,7 @@ class DataAccessTracker:
|
|
377
383
|
# Check if trifecta is already achieved before processing this call
|
378
384
|
if self.is_trifecta_achieved():
|
379
385
|
log.error(f"🚫 BLOCKING tool call {tool_name} - lethal trifecta already achieved")
|
386
|
+
record_tool_call_blocked(tool_name, "trifecta")
|
380
387
|
raise SecurityError(f"Tool call '{tool_name}' blocked: lethal trifecta achieved")
|
381
388
|
|
382
389
|
# Get tool permissions and update trifecta flags
|
@@ -387,19 +394,23 @@ class DataAccessTracker:
|
|
387
394
|
# Check if tool is enabled
|
388
395
|
if permissions["enabled"] is False:
|
389
396
|
log.warning(f"🚫 BLOCKING tool call {tool_name} - tool is disabled")
|
397
|
+
record_tool_call_blocked(tool_name, "disabled")
|
390
398
|
raise SecurityError(f"Tool call '{tool_name}' blocked: tool is disabled")
|
391
399
|
|
392
400
|
if permissions["read_private_data"]:
|
393
401
|
self.has_private_data_access = True
|
394
402
|
log.info(f"🔒 Private data access detected: {tool_name}")
|
403
|
+
record_private_data_access("tool", tool_name)
|
395
404
|
|
396
405
|
if permissions["read_untrusted_public_data"]:
|
397
406
|
self.has_untrusted_content_exposure = True
|
398
407
|
log.info(f"🌐 Untrusted content exposure detected: {tool_name}")
|
408
|
+
record_untrusted_public_data("tool", tool_name)
|
399
409
|
|
400
410
|
if permissions["write_operation"]:
|
401
411
|
self.has_external_communication = True
|
402
412
|
log.info(f"✍️ Write operation detected: {tool_name}")
|
413
|
+
record_write_operation("tool", tool_name)
|
403
414
|
|
404
415
|
# Log if trifecta is achieved after this call
|
405
416
|
if self.is_trifecta_achieved():
|
@@ -435,14 +446,17 @@ class DataAccessTracker:
|
|
435
446
|
if permissions["read_private_data"]:
|
436
447
|
self.has_private_data_access = True
|
437
448
|
log.info(f"🔒 Private data access detected via resource: {resource_name}")
|
449
|
+
record_private_data_access("resource", resource_name)
|
438
450
|
|
439
451
|
if permissions["read_untrusted_public_data"]:
|
440
452
|
self.has_untrusted_content_exposure = True
|
441
453
|
log.info(f"🌐 Untrusted content exposure detected via resource: {resource_name}")
|
454
|
+
record_untrusted_public_data("resource", resource_name)
|
442
455
|
|
443
456
|
if permissions["write_operation"]:
|
444
457
|
self.has_external_communication = True
|
445
458
|
log.info(f"✍️ Write operation detected via resource: {resource_name}")
|
459
|
+
record_write_operation("resource", resource_name)
|
446
460
|
|
447
461
|
# Log if trifecta is achieved after this access
|
448
462
|
if self.is_trifecta_achieved():
|
@@ -474,14 +488,17 @@ class DataAccessTracker:
|
|
474
488
|
if permissions["read_private_data"]:
|
475
489
|
self.has_private_data_access = True
|
476
490
|
log.info(f"🔒 Private data access detected via prompt: {prompt_name}")
|
491
|
+
record_private_data_access("prompt", prompt_name)
|
477
492
|
|
478
493
|
if permissions["read_untrusted_public_data"]:
|
479
494
|
self.has_untrusted_content_exposure = True
|
480
495
|
log.info(f"🌐 Untrusted content exposure detected via prompt: {prompt_name}")
|
496
|
+
record_untrusted_public_data("prompt", prompt_name)
|
481
497
|
|
482
498
|
if permissions["write_operation"]:
|
483
499
|
self.has_external_communication = True
|
484
500
|
log.info(f"✍️ Write operation detected via prompt: {prompt_name}")
|
501
|
+
record_write_operation("prompt", prompt_name)
|
485
502
|
|
486
503
|
# Log if trifecta is achieved after this access
|
487
504
|
if self.is_trifecta_achieved():
|
@@ -29,6 +29,11 @@ from sqlalchemy.sql import select
|
|
29
29
|
|
30
30
|
from src.config import get_config_dir # type: ignore[reportMissingImports]
|
31
31
|
from src.middleware.data_access_tracker import DataAccessTracker
|
32
|
+
from src.telemetry import (
|
33
|
+
record_prompt_used,
|
34
|
+
record_resource_used,
|
35
|
+
record_tool_call,
|
36
|
+
)
|
32
37
|
|
33
38
|
|
34
39
|
@dataclass
|
@@ -285,6 +290,8 @@ class SessionTrackingMiddleware(Middleware):
|
|
285
290
|
assert session.data_access_tracker is not None
|
286
291
|
log.debug(f"🔍 Analyzing tool {context.message.name} for security implications")
|
287
292
|
_ = session.data_access_tracker.add_tool_call(context.message.name)
|
293
|
+
# Telemetry: record tool call
|
294
|
+
record_tool_call(context.message.name)
|
288
295
|
|
289
296
|
# Update database session
|
290
297
|
with create_db_session() as db_session:
|
@@ -383,6 +390,7 @@ class SessionTrackingMiddleware(Middleware):
|
|
383
390
|
|
384
391
|
log.debug(f"🔍 Analyzing resource {resource_name} for security implications")
|
385
392
|
_ = session.data_access_tracker.add_resource_access(resource_name)
|
393
|
+
record_resource_used(resource_name)
|
386
394
|
|
387
395
|
# Update database session
|
388
396
|
with create_db_session() as db_session:
|
@@ -463,6 +471,7 @@ class SessionTrackingMiddleware(Middleware):
|
|
463
471
|
|
464
472
|
log.debug(f"🔍 Analyzing prompt {prompt_name} for security implications")
|
465
473
|
_ = session.data_access_tracker.add_prompt_access(prompt_name)
|
474
|
+
record_prompt_used(prompt_name)
|
466
475
|
|
467
476
|
# Update database session
|
468
477
|
with create_db_session() as db_session:
|
src/server.py
CHANGED
@@ -26,6 +26,7 @@ from src.middleware.session_tracking import (
|
|
26
26
|
create_db_session,
|
27
27
|
)
|
28
28
|
from src.single_user_mcp import SingleUserMCP
|
29
|
+
from src.telemetry import initialize_telemetry, set_servers_installed
|
29
30
|
|
30
31
|
|
31
32
|
def _get_current_config():
|
@@ -267,6 +268,8 @@ class OpenEdisonProxy:
|
|
267
268
|
log.info(f"FastAPI management API on {self.host}:{self.port + 1}")
|
268
269
|
log.info(f"FastMCP protocol server on {self.host}:{self.port}")
|
269
270
|
|
271
|
+
initialize_telemetry()
|
272
|
+
|
270
273
|
# Ensure the sessions database exists and has the required schema
|
271
274
|
try:
|
272
275
|
with create_db_session():
|
@@ -277,6 +280,10 @@ class OpenEdisonProxy:
|
|
277
280
|
# Initialize the FastMCP server (this handles starting enabled MCP servers)
|
278
281
|
await self.single_user_mcp.initialize()
|
279
282
|
|
283
|
+
# Emit snapshot of enabled servers
|
284
|
+
enabled_count = len([s for s in config.mcp_servers if s.enabled])
|
285
|
+
set_servers_installed(enabled_count)
|
286
|
+
|
280
287
|
# Add CORS middleware to FastAPI
|
281
288
|
self.fastapi_app.add_middleware(
|
282
289
|
CORSMiddleware,
|
src/telemetry.py
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
"""
|
2
|
+
Telemetry for Open Edison (opt-out).
|
3
|
+
This module provides a thin, optional wrapper around OpenTelemetry to export
|
4
|
+
basic usage metrics to an OTLP endpoint. If telemetry is disabled or the
|
5
|
+
OpenTelemetry packages are not installed, all functions are safe no-ops.
|
6
|
+
|
7
|
+
Events/metrics captured (high level, install-unique ID for deaggregation):
|
8
|
+
- tool_calls_total (counter)
|
9
|
+
- tool_calls_blocked_total (counter)
|
10
|
+
- servers_installed_total (up-down counter / gauge)
|
11
|
+
- tool_calls_metadata_total (counter)
|
12
|
+
- resource_used_total (counter)
|
13
|
+
- prompt_used_total (counter)
|
14
|
+
- private_data_access_calls_total (counter)
|
15
|
+
- untrusted_public_data_calls_total (counter)
|
16
|
+
- write_operation_calls_total (counter)
|
17
|
+
|
18
|
+
Configuration: see `TelemetryConfig` in `src.config`.
|
19
|
+
"""
|
20
|
+
|
21
|
+
from __future__ import annotations
|
22
|
+
|
23
|
+
import json
|
24
|
+
import os
|
25
|
+
import traceback
|
26
|
+
import uuid
|
27
|
+
from collections.abc import Callable
|
28
|
+
from typing import Any, ParamSpec, TypeVar
|
29
|
+
|
30
|
+
from loguru import logger as log
|
31
|
+
|
32
|
+
# OpenTelemetry metrics components
|
33
|
+
from opentelemetry import metrics as ot_metrics
|
34
|
+
from opentelemetry.exporter.otlp.proto.http import metric_exporter as otlp_metric_exporter
|
35
|
+
from opentelemetry.sdk import metrics as ot_sdk_metrics
|
36
|
+
from opentelemetry.sdk.metrics import export as ot_metrics_export
|
37
|
+
from opentelemetry.sdk.resources import Resource # type: ignore[reportMissingTypeStubs]
|
38
|
+
|
39
|
+
from src.config import TelemetryConfig, config, get_config_dir
|
40
|
+
|
41
|
+
_initialized: bool = False
|
42
|
+
_install_id: str | None = None
|
43
|
+
_provider: Any | None = None
|
44
|
+
_tool_calls_counter: Any | None = None
|
45
|
+
_tool_calls_blocked_counter: Any | None = None
|
46
|
+
_servers_installed_gauge: Any | None = None
|
47
|
+
_tool_calls_metadata_counter: Any | None = None
|
48
|
+
_resource_used_counter: Any | None = None
|
49
|
+
_prompt_used_counter: Any | None = None
|
50
|
+
_private_data_access_counter: Any | None = None
|
51
|
+
_untrusted_public_data_counter: Any | None = None
|
52
|
+
_write_operation_counter: Any | None = None
|
53
|
+
|
54
|
+
|
55
|
+
def _ensure_install_id() -> str:
|
56
|
+
"""Create or read a persistent install-unique ID under the config dir."""
|
57
|
+
global _install_id
|
58
|
+
if _install_id:
|
59
|
+
return _install_id
|
60
|
+
try:
|
61
|
+
cfg_dir = get_config_dir()
|
62
|
+
cfg_dir.mkdir(parents=True, exist_ok=True)
|
63
|
+
except Exception: # noqa: BLE001
|
64
|
+
log.error(
|
65
|
+
"Could not resolve or create config dir for install_id; using ephemeral ID\n{}",
|
66
|
+
traceback.format_exc(),
|
67
|
+
)
|
68
|
+
_install_id = str(uuid.uuid4())
|
69
|
+
return _install_id
|
70
|
+
|
71
|
+
id_file = cfg_dir / "install_id"
|
72
|
+
if id_file.exists():
|
73
|
+
try:
|
74
|
+
_install_id = id_file.read_text(encoding="utf-8").strip() or str(uuid.uuid4())
|
75
|
+
except Exception: # noqa: BLE001
|
76
|
+
log.error(
|
77
|
+
"Failed reading install_id file; using ephemeral ID\n{}",
|
78
|
+
traceback.format_exc(),
|
79
|
+
)
|
80
|
+
_install_id = str(uuid.uuid4())
|
81
|
+
else:
|
82
|
+
_install_id = str(uuid.uuid4())
|
83
|
+
try:
|
84
|
+
id_file.write_text(_install_id, encoding="utf-8")
|
85
|
+
except Exception: # noqa: BLE001
|
86
|
+
log.error(
|
87
|
+
"Failed writing install_id file; continuing without persistence\n{}",
|
88
|
+
traceback.format_exc(),
|
89
|
+
)
|
90
|
+
return _install_id
|
91
|
+
|
92
|
+
|
93
|
+
def _telemetry_enabled() -> bool:
|
94
|
+
tel_cfg = config.telemetry or TelemetryConfig()
|
95
|
+
return bool(tel_cfg.enabled)
|
96
|
+
|
97
|
+
|
98
|
+
P = ParamSpec("P")
|
99
|
+
R = TypeVar("R")
|
100
|
+
|
101
|
+
|
102
|
+
def telemetry_recorder(func: Callable[P, R]) -> Callable[P, R | None]: # noqa: UP047
|
103
|
+
"""No-op when disabled, ensure init, and catch/log failures."""
|
104
|
+
|
105
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None: # type: ignore[override]
|
106
|
+
if not _telemetry_enabled():
|
107
|
+
return None
|
108
|
+
if not _initialized:
|
109
|
+
initialize_telemetry()
|
110
|
+
try:
|
111
|
+
return func(*args, **kwargs)
|
112
|
+
except Exception: # noqa: BLE001
|
113
|
+
log.error("Telemetry emit failed\n{}", traceback.format_exc())
|
114
|
+
return None
|
115
|
+
|
116
|
+
return wrapper
|
117
|
+
|
118
|
+
|
119
|
+
def initialize_telemetry(override: TelemetryConfig | None = None) -> None:
|
120
|
+
"""Initialize telemetry if enabled in config.
|
121
|
+
|
122
|
+
Safe to call multiple times; only first call initializes.
|
123
|
+
"""
|
124
|
+
global \
|
125
|
+
_initialized, \
|
126
|
+
_provider, \
|
127
|
+
_tool_calls_counter, \
|
128
|
+
_tool_calls_blocked_counter, \
|
129
|
+
_servers_installed_gauge
|
130
|
+
|
131
|
+
if _initialized:
|
132
|
+
return
|
133
|
+
|
134
|
+
telemetry_cfg = override if override is not None else (config.telemetry or TelemetryConfig())
|
135
|
+
if not telemetry_cfg.enabled:
|
136
|
+
log.debug("Telemetry disabled by config")
|
137
|
+
_initialized = True
|
138
|
+
return
|
139
|
+
|
140
|
+
# Exporter
|
141
|
+
exporter_kwargs: dict[str, Any] = {}
|
142
|
+
if telemetry_cfg.otlp_endpoint:
|
143
|
+
exporter_kwargs["endpoint"] = telemetry_cfg.otlp_endpoint
|
144
|
+
# Allow environment variables to provide endpoint when not set in config
|
145
|
+
env_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT") or os.environ.get(
|
146
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT"
|
147
|
+
)
|
148
|
+
if "endpoint" not in exporter_kwargs and env_endpoint:
|
149
|
+
exporter_kwargs["endpoint"] = env_endpoint
|
150
|
+
# If no endpoint is available from config or env, skip initialization quietly
|
151
|
+
if "endpoint" not in exporter_kwargs:
|
152
|
+
log.debug("No OTLP endpoint configured (config or env); skipping telemetry init")
|
153
|
+
_initialized = True
|
154
|
+
return
|
155
|
+
if telemetry_cfg.headers:
|
156
|
+
exporter_kwargs["headers"] = telemetry_cfg.headers
|
157
|
+
|
158
|
+
try:
|
159
|
+
exporter: Any = otlp_metric_exporter.OTLPMetricExporter(**exporter_kwargs)
|
160
|
+
except Exception: # noqa: BLE001
|
161
|
+
log.error("OTLP exporter init failed\n{}", traceback.format_exc())
|
162
|
+
return
|
163
|
+
|
164
|
+
# Reader
|
165
|
+
try:
|
166
|
+
reader: Any = ot_metrics_export.PeriodicExportingMetricReader(
|
167
|
+
exporter=exporter,
|
168
|
+
export_interval_millis=max(1000, telemetry_cfg.export_interval_ms),
|
169
|
+
)
|
170
|
+
except Exception: # noqa: BLE001
|
171
|
+
log.error("OTLP reader init failed\n{}", traceback.format_exc())
|
172
|
+
return
|
173
|
+
|
174
|
+
# Provider/meter
|
175
|
+
try:
|
176
|
+
# Attach a resource so metrics include service identifiers
|
177
|
+
resource = Resource.create(
|
178
|
+
{
|
179
|
+
"service.name": "open-edison",
|
180
|
+
"service.namespace": "open-edison",
|
181
|
+
"telemetry.sdk.language": "python",
|
182
|
+
}
|
183
|
+
)
|
184
|
+
provider: Any = ot_sdk_metrics.MeterProvider(metric_readers=[reader], resource=resource)
|
185
|
+
_provider = provider
|
186
|
+
ot_metrics.set_meter_provider(provider)
|
187
|
+
meter: Any = ot_metrics.get_meter("open-edison")
|
188
|
+
except Exception: # noqa: BLE001
|
189
|
+
log.error("Metrics provider init failed\n{}", traceback.format_exc())
|
190
|
+
return
|
191
|
+
|
192
|
+
# Instruments
|
193
|
+
try:
|
194
|
+
# Do not suffix counters with _total; Prometheus exporter appends it
|
195
|
+
_tool_calls_counter = meter.create_counter("tool_calls")
|
196
|
+
_tool_calls_blocked_counter = meter.create_counter("tool_calls_blocked")
|
197
|
+
_servers_installed_gauge = meter.create_up_down_counter("servers_installed")
|
198
|
+
_tool_calls_metadata_counter = meter.create_counter("tool_calls_metadata")
|
199
|
+
_resource_used_counter = meter.create_counter("resource_used")
|
200
|
+
_prompt_used_counter = meter.create_counter("prompt_used")
|
201
|
+
_private_data_access_counter = meter.create_counter("private_data_access_calls")
|
202
|
+
_untrusted_public_data_counter = meter.create_counter("untrusted_public_data_calls")
|
203
|
+
_write_operation_counter = meter.create_counter("write_operation_calls")
|
204
|
+
except Exception: # noqa: BLE001
|
205
|
+
log.error("Metrics instrument creation failed\n{}", traceback.format_exc())
|
206
|
+
return
|
207
|
+
|
208
|
+
_ = _ensure_install_id()
|
209
|
+
_initialized = True
|
210
|
+
log.info("📈 Telemetry initialized")
|
211
|
+
|
212
|
+
|
213
|
+
def force_flush_metrics(timeout_ms: int = 5000) -> bool:
|
214
|
+
"""Force-flush metrics synchronously if a provider is initialized.
|
215
|
+
|
216
|
+
Returns True on success, False otherwise.
|
217
|
+
"""
|
218
|
+
try:
|
219
|
+
provider = _provider
|
220
|
+
if provider is None:
|
221
|
+
return False
|
222
|
+
# Some providers expose force_flush(timeout_millis=...), others as force_flush() -> bool
|
223
|
+
if hasattr(provider, "force_flush"):
|
224
|
+
try:
|
225
|
+
# Try with timeout argument first
|
226
|
+
result = provider.force_flush(timeout_millis=timeout_ms) # type: ignore[misc]
|
227
|
+
except TypeError:
|
228
|
+
result = provider.force_flush()
|
229
|
+
return bool(result)
|
230
|
+
return False
|
231
|
+
except Exception: # noqa: BLE001
|
232
|
+
log.error("Force flush failed\n{}", traceback.format_exc())
|
233
|
+
return False
|
234
|
+
|
235
|
+
|
236
|
+
def _common_attrs(extra: dict[str, Any] | None = None) -> dict[str, Any]:
|
237
|
+
attrs: dict[str, Any] = {"install_id": _ensure_install_id(), "app": "open-edison"}
|
238
|
+
if extra:
|
239
|
+
attrs.update(extra)
|
240
|
+
return attrs
|
241
|
+
|
242
|
+
|
243
|
+
@telemetry_recorder
|
244
|
+
def record_tool_call(tool_name: str) -> None:
|
245
|
+
if _tool_calls_counter is None:
|
246
|
+
return
|
247
|
+
_tool_calls_counter.add(1, attributes=_common_attrs({"tool": tool_name}))
|
248
|
+
|
249
|
+
|
250
|
+
@telemetry_recorder
|
251
|
+
def record_tool_call_blocked(tool_name: str, reason: str) -> None:
|
252
|
+
if _tool_calls_blocked_counter is None:
|
253
|
+
return
|
254
|
+
_tool_calls_blocked_counter.add(
|
255
|
+
1, attributes=_common_attrs({"tool": tool_name, "reason": reason})
|
256
|
+
)
|
257
|
+
|
258
|
+
|
259
|
+
@telemetry_recorder
|
260
|
+
def record_tool_call_metadata(tool_name: str, metadata: dict[str, Any]) -> None:
|
261
|
+
if _tool_calls_metadata_counter is None:
|
262
|
+
return
|
263
|
+
metadata_str = json.dumps(metadata, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
264
|
+
_tool_calls_metadata_counter.add(
|
265
|
+
1, attributes=_common_attrs({"tool": tool_name, "metadata_json": metadata_str})
|
266
|
+
)
|
267
|
+
|
268
|
+
|
269
|
+
@telemetry_recorder
|
270
|
+
def set_servers_installed(count: int) -> None:
|
271
|
+
if _servers_installed_gauge is None:
|
272
|
+
return
|
273
|
+
_servers_installed_gauge.add(count, attributes=_common_attrs({"state": "snapshot"}))
|
274
|
+
|
275
|
+
|
276
|
+
@telemetry_recorder
|
277
|
+
def record_resource_used(resource_name: str) -> None:
|
278
|
+
if _resource_used_counter is None:
|
279
|
+
return
|
280
|
+
_resource_used_counter.add(1, attributes=_common_attrs({"resource": resource_name}))
|
281
|
+
|
282
|
+
|
283
|
+
@telemetry_recorder
|
284
|
+
def record_prompt_used(prompt_name: str) -> None:
|
285
|
+
if _prompt_used_counter is None:
|
286
|
+
return
|
287
|
+
_prompt_used_counter.add(1, attributes=_common_attrs({"prompt": prompt_name}))
|
288
|
+
|
289
|
+
|
290
|
+
@telemetry_recorder
|
291
|
+
def record_private_data_access(source_type: str, name: str) -> None:
|
292
|
+
if _private_data_access_counter is None:
|
293
|
+
return
|
294
|
+
_private_data_access_counter.add(
|
295
|
+
1, attributes=_common_attrs({"source_type": source_type, "name": name})
|
296
|
+
)
|
297
|
+
|
298
|
+
|
299
|
+
@telemetry_recorder
|
300
|
+
def record_untrusted_public_data(source_type: str, name: str) -> None:
|
301
|
+
if _untrusted_public_data_counter is None:
|
302
|
+
return
|
303
|
+
_untrusted_public_data_counter.add(
|
304
|
+
1, attributes=_common_attrs({"source_type": source_type, "name": name})
|
305
|
+
)
|
306
|
+
|
307
|
+
|
308
|
+
@telemetry_recorder
|
309
|
+
def record_write_operation(source_type: str, name: str) -> None:
|
310
|
+
if _write_operation_counter is None:
|
311
|
+
return
|
312
|
+
_write_operation_counter.add(
|
313
|
+
1, attributes=_common_attrs({"source_type": source_type, "name": name})
|
314
|
+
)
|
@@ -1,17 +0,0 @@
|
|
1
|
-
src/__init__.py,sha256=QWeZdjAm2D2B0eWhd8m2-DPpWvIP26KcNJxwEoU1oEQ,254
|
2
|
-
src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
|
3
|
-
src/cli.py,sha256=ketV-e9oQMVlLBjZR7YbK33XkEfqxPyzWqYkS1YwqYc,9968
|
4
|
-
src/config.py,sha256=klWrNycPxzVt9wPhiNbjXMkB4bHZplenfWDx-3UtQac,7120
|
5
|
-
src/mcp_manager.py,sha256=VpRdVMy1WLegC-gBnyTcBMcKzQsdIn4JIWuHf7Q40hg,4442
|
6
|
-
src/server.py,sha256=7hwhutP0qZ_mjZfs6jcB-UNe_VyibFKl6hPyHWoa-ns,22896
|
7
|
-
src/single_user_mcp.py,sha256=ue5UnC0nfmuLR4z87904WqH7B-0FaACFDWaBNNL7hXE,15259
|
8
|
-
src/frontend_dist/index.html,sha256=CL9uiDUygp5_5_VpsW4WMgYFsMAfVSueYit_vFgX0Qo,673
|
9
|
-
src/frontend_dist/assets/index-CKkid2y-.js,sha256=zaZ7j0nyGkywXAMuCrhZLaSOVqLu7JkQG3wE_8QiFT4,219537
|
10
|
-
src/frontend_dist/assets/index-CRxojymD.css,sha256=kANM9zPkbS5aLrPzePZK0Fbt580I6kNnyFjkFH13HtA,11383
|
11
|
-
src/middleware/data_access_tracker.py,sha256=JkwZdtMCiVU7JJZDd-GhlowW2szMDnXrD95nhxQVXR4,21165
|
12
|
-
src/middleware/session_tracking.py,sha256=rWZh4UBQbqzPh4p6vxdtRwEC1uzq93yjzxcI9LnlRkA,19307
|
13
|
-
open_edison-0.1.10.dist-info/METADATA,sha256=15i5EIVlRNQtBIs3RJTTwiTPXEfF2FYy2a3W2KoBN3g,8834
|
14
|
-
open_edison-0.1.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
-
open_edison-0.1.10.dist-info/entry_points.txt,sha256=qNAkJcnoTXRhj8J--3PDmXz_TQKdB8H_0C9wiCtDIyA,72
|
16
|
-
open_edison-0.1.10.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
17
|
-
open_edison-0.1.10.dist-info/RECORD,,
|