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.
@@ -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}}
@@ -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-CKkid2y-.js"></script>
14
- <link rel="stylesheet" crossorigin href="/assets/index-CRxojymD.css">
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,,