cyris 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cyris-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cyris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
cyris-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: cyris
3
+ Version: 0.1.0
4
+ Summary: The system of record for AI agent decisions. Log every LLM call your agents make, hash-chain the audit trail, and answer compliance questionnaires from real data.
5
+ Author-email: Cyris <hello@cyrisai.dev>
6
+ License: MIT
7
+ Project-URL: Homepage, https://cyrisai.dev
8
+ Project-URL: Repository, https://github.com/ultimatem7/Cyris
9
+ Project-URL: Issues, https://github.com/ultimatem7/Cyris/issues
10
+ Keywords: ai,audit,compliance,hipaa,llm,observability,openai,anthropic,agents,monitoring
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Healthcare Industry
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Monitoring
24
+ Classifier: Topic :: System :: Logging
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: requests>=2.20.0
29
+ Provides-Extra: openai
30
+ Requires-Dist: openai>=1.0.0; extra == "openai"
31
+ Provides-Extra: anthropic
32
+ Requires-Dist: anthropic>=0.18.0; extra == "anthropic"
33
+ Provides-Extra: all
34
+ Requires-Dist: openai>=1.0.0; extra == "all"
35
+ Requires-Dist: anthropic>=0.18.0; extra == "all"
36
+ Dynamic: license-file
37
+
38
+ # cyris
39
+
40
+ The system of record for AI agent decisions.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install cyris
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ import cyris
52
+ cyris.init(api_key="your-key")
53
+ # That's it. Every OpenAI/Anthropic call is now logged.
54
+ ```
55
+
56
+ ## Manual logging
57
+
58
+ ```python
59
+ cyris.log_call(provider="openai", model="gpt-4o", input_text="Hello", output_text="Hi there")
60
+ ```
61
+
62
+ ## Sessions
63
+
64
+ ```python
65
+ with cyris.session("my-workflow") as sid:
66
+ # all calls inside this block share the same session_id
67
+ ...
68
+ ```
69
+
70
+ ## CLI
71
+
72
+ ```bash
73
+ cyris login # authenticate via browser
74
+ cyris whoami # show current identity
75
+ cyris status # check API connectivity
76
+ cyris logout # remove saved credentials
77
+ ```
cyris-0.1.0/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # cyris
2
+
3
+ The system of record for AI agent decisions.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install cyris
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ import cyris
15
+ cyris.init(api_key="your-key")
16
+ # That's it. Every OpenAI/Anthropic call is now logged.
17
+ ```
18
+
19
+ ## Manual logging
20
+
21
+ ```python
22
+ cyris.log_call(provider="openai", model="gpt-4o", input_text="Hello", output_text="Hi there")
23
+ ```
24
+
25
+ ## Sessions
26
+
27
+ ```python
28
+ with cyris.session("my-workflow") as sid:
29
+ # all calls inside this block share the same session_id
30
+ ...
31
+ ```
32
+
33
+ ## CLI
34
+
35
+ ```bash
36
+ cyris login # authenticate via browser
37
+ cyris whoami # show current identity
38
+ cyris status # check API connectivity
39
+ cyris logout # remove saved credentials
40
+ ```
@@ -0,0 +1,5 @@
1
+ from .core import init, log_call, log_override, log_event
2
+ from .session import session
3
+ from .version import __version__
4
+
5
+ __all__ = ["init", "log_call", "log_override", "log_event", "session", "__version__"]
@@ -0,0 +1,126 @@
1
+ """
2
+ Thread-safe event buffer with background flushing.
3
+
4
+ Events are queued in memory and periodically POSTed to the Cyris
5
+ ``/ingest-batch`` endpoint by a daemon thread. The design guarantees:
6
+
7
+ * **No perceptible latency** -- callers return immediately after enqueue.
8
+ * **No data loss on exit** -- ``atexit`` flushes remaining events.
9
+ * **Silent failures** -- network errors go to *stderr*, never raise.
10
+ """
11
+
12
+ import atexit
13
+ import json
14
+ import sys
15
+ import threading
16
+ import time
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ _queue: List[Dict[str, Any]] = []
20
+ _lock = threading.Lock()
21
+ _flush_event = threading.Event()
22
+
23
+ _api_url: Optional[str] = None
24
+ _api_key: Optional[str] = None
25
+ _thread: Optional[threading.Thread] = None
26
+ _shutdown_flag = threading.Event()
27
+
28
+ _FLUSH_INTERVAL_S = 5
29
+ _FLUSH_BATCH_SIZE = 10
30
+
31
+
32
+ # ---------------------------------------------------------------------- #
33
+ # Public API #
34
+ # ---------------------------------------------------------------------- #
35
+
36
+ def configure(api_url: str, api_key: str) -> None:
37
+ """Set the target endpoint and API key. Called once from ``init()``."""
38
+ global _api_url, _api_key
39
+ _api_url = api_url.rstrip("/")
40
+ _api_key = api_key
41
+
42
+
43
+ def start() -> None:
44
+ """Spin up the background flush thread (idempotent)."""
45
+ global _thread
46
+ if _thread is not None and _thread.is_alive():
47
+ return
48
+
49
+ _shutdown_flag.clear()
50
+ _thread = threading.Thread(target=_flush_loop, daemon=True, name="cyris-buffer")
51
+ _thread.start()
52
+ atexit.register(shutdown)
53
+
54
+
55
+ def add_event(event: Dict[str, Any]) -> None:
56
+ """Enqueue an event dict for later transmission."""
57
+ with _lock:
58
+ _queue.append(event)
59
+ if len(_queue) >= _FLUSH_BATCH_SIZE:
60
+ _flush_event.set()
61
+
62
+
63
+ def flush() -> None:
64
+ """Immediately flush the current queue (best-effort)."""
65
+ _do_flush()
66
+
67
+
68
+ def shutdown() -> None:
69
+ """Signal the background thread to stop and flush remaining events."""
70
+ _shutdown_flag.set()
71
+ _flush_event.set() # wake the thread so it exits promptly
72
+ if _thread is not None and _thread.is_alive():
73
+ _thread.join(timeout=5)
74
+ _do_flush()
75
+
76
+
77
+ # ---------------------------------------------------------------------- #
78
+ # Internals #
79
+ # ---------------------------------------------------------------------- #
80
+
81
+ def _flush_loop() -> None:
82
+ """Background loop: flush every *_FLUSH_INTERVAL_S* or on signal."""
83
+ while not _shutdown_flag.is_set():
84
+ _flush_event.wait(timeout=_FLUSH_INTERVAL_S)
85
+ _flush_event.clear()
86
+ _do_flush()
87
+
88
+
89
+ def _do_flush() -> None:
90
+ """Drain the queue and POST to the ingest endpoint."""
91
+ with _lock:
92
+ if not _queue:
93
+ return
94
+ batch = list(_queue)
95
+ _queue.clear()
96
+
97
+ if not _api_url or not _api_key:
98
+ return
99
+
100
+ try:
101
+ import requests # deferred so import-time stays fast
102
+
103
+ url = f"{_api_url}/ingest-batch"
104
+ headers = {
105
+ "Authorization": f"Bearer {_api_key}",
106
+ "Content-Type": "application/json",
107
+ }
108
+ # Server expects { "events": [...] }. Sending a raw list returns 400.
109
+ payload = json.dumps({"events": batch})
110
+ resp = requests.post(url, data=payload, headers=headers, timeout=10)
111
+
112
+ # Surface non-2xx in stderr so users notice silently-dropped events.
113
+ if not (200 <= resp.status_code < 300):
114
+ try:
115
+ print(
116
+ f"[cyris] flush rejected: HTTP {resp.status_code} {resp.text[:200]}",
117
+ file=sys.stderr,
118
+ )
119
+ except Exception:
120
+ pass
121
+ except Exception as exc: # noqa: BLE001 -- intentionally broad
122
+ # Silent failure: never crash the host application.
123
+ try:
124
+ print(f"[cyris] flush error: {exc}", file=sys.stderr)
125
+ except Exception:
126
+ pass
@@ -0,0 +1,202 @@
1
+ """
2
+ CLI entry point for the ``cyris`` command.
3
+
4
+ Subcommands
5
+ -----------
6
+ cyris login -- authenticate via browser OAuth flow
7
+ cyris logout -- remove saved credentials
8
+ cyris whoami -- display the current identity
9
+ cyris status -- check API connectivity
10
+ cyris --version -- print package version
11
+ """
12
+
13
+ import argparse
14
+ import http.server
15
+ import json
16
+ import secrets
17
+ import sys
18
+ import threading
19
+ import urllib.parse
20
+ import webbrowser
21
+ from typing import Optional
22
+
23
+ from .credentials import delete_credentials, load_credentials, save_credentials
24
+ from .version import __version__
25
+
26
+ _DEFAULT_API_URL = "https://cyrisai.dev/api/v1"
27
+
28
+
29
+ # ====================================================================== #
30
+ # CLI dispatcher #
31
+ # ====================================================================== #
32
+
33
+ def main(argv: Optional[list] = None) -> None:
34
+ parser = argparse.ArgumentParser(
35
+ prog="cyris",
36
+ description="Cyris -- the system of record for AI agent decisions.",
37
+ )
38
+ parser.add_argument(
39
+ "--version", "-V",
40
+ action="version",
41
+ version=f"cyris {__version__}",
42
+ )
43
+
44
+ sub = parser.add_subparsers(dest="command")
45
+
46
+ # login
47
+ login_p = sub.add_parser("login", help="Authenticate with Cyris")
48
+ login_p.add_argument("--api-url", default=_DEFAULT_API_URL, help="API base URL")
49
+
50
+ # logout
51
+ sub.add_parser("logout", help="Remove saved credentials")
52
+
53
+ # whoami
54
+ sub.add_parser("whoami", help="Show current identity")
55
+
56
+ # status
57
+ status_p = sub.add_parser("status", help="Check API connectivity")
58
+ status_p.add_argument("--api-url", default=None, help="Override API URL")
59
+
60
+ args = parser.parse_args(argv)
61
+
62
+ if args.command == "login":
63
+ _cmd_login(args.api_url)
64
+ elif args.command == "logout":
65
+ _cmd_logout()
66
+ elif args.command == "whoami":
67
+ _cmd_whoami()
68
+ elif args.command == "status":
69
+ _cmd_status(args.api_url)
70
+ else:
71
+ parser.print_help()
72
+
73
+
74
+ # ====================================================================== #
75
+ # Subcommand implementations #
76
+ # ====================================================================== #
77
+
78
+ def _cmd_login(api_url: str) -> None:
79
+ """Open the browser for OAuth, start a local HTTP server for callback."""
80
+ api_url = api_url.rstrip("/")
81
+ state = secrets.token_urlsafe(32)
82
+
83
+ # Pick an ephemeral port for the callback server.
84
+ result: dict = {}
85
+ server_ready = threading.Event()
86
+
87
+ class _CallbackHandler(http.server.BaseHTTPRequestHandler):
88
+ def do_GET(self) -> None: # noqa: N802
89
+ query = urllib.parse.urlparse(self.path).query
90
+ params = urllib.parse.parse_qs(query)
91
+
92
+ received_state = params.get("state", [None])[0]
93
+ api_key = params.get("api_key", [None])[0]
94
+ org_name = params.get("org_name", [""])[0]
95
+ email = params.get("email", [""])[0]
96
+
97
+ if received_state != state:
98
+ self.send_response(400)
99
+ self.end_headers()
100
+ self.wfile.write(b"State mismatch -- login aborted.")
101
+ return
102
+
103
+ result["api_key"] = api_key
104
+ result["org_name"] = org_name
105
+ result["email"] = email
106
+
107
+ self.send_response(200)
108
+ self.send_header("Content-Type", "text/html")
109
+ self.end_headers()
110
+ self.wfile.write(
111
+ b"<html><body><h2>Login successful!</h2>"
112
+ b"<p>You can close this tab.</p></body></html>"
113
+ )
114
+
115
+ def log_message(self, fmt, *args) -> None: # noqa: ARG002
116
+ pass # silence server logs
117
+
118
+ server = http.server.HTTPServer(("127.0.0.1", 0), _CallbackHandler)
119
+ port = server.server_address[1]
120
+
121
+ def _serve() -> None:
122
+ server_ready.set()
123
+ server.handle_request() # single request then stop
124
+
125
+ t = threading.Thread(target=_serve, daemon=True)
126
+ t.start()
127
+ server_ready.wait()
128
+
129
+ callback = f"http://localhost:{port}"
130
+ login_url = f"{api_url}/auth/cli?callback={urllib.parse.quote(callback)}&state={state}"
131
+
132
+ print(f"Opening browser for login...\n{login_url}")
133
+ webbrowser.open(login_url)
134
+
135
+ t.join(timeout=120)
136
+ server.server_close()
137
+
138
+ if result.get("api_key"):
139
+ save_credentials(
140
+ api_key=result["api_key"],
141
+ api_url=api_url,
142
+ org_name=result.get("org_name", ""),
143
+ email=result.get("email", ""),
144
+ )
145
+ print(f"Logged in as {result.get('email', 'unknown')} ({result.get('org_name', '')})")
146
+ else:
147
+ print("Login failed or timed out.", file=sys.stderr)
148
+ sys.exit(1)
149
+
150
+
151
+ def _cmd_logout() -> None:
152
+ delete_credentials()
153
+ print("Logged out. Credentials removed.")
154
+
155
+
156
+ def _cmd_whoami() -> None:
157
+ creds = load_credentials()
158
+ if not creds:
159
+ print("Not logged in. Run `cyris login` first.")
160
+ return
161
+ print(f"Email: {creds.get('email', '(not set)')}")
162
+ print(f"Org: {creds.get('org_name', '(not set)')}")
163
+ print(f"API URL: {creds.get('api_url', _DEFAULT_API_URL)}")
164
+
165
+
166
+ def _cmd_status(api_url_override: Optional[str]) -> None:
167
+ creds = load_credentials()
168
+ api_url = api_url_override or (creds and creds.get("api_url")) or _DEFAULT_API_URL
169
+ api_key = (creds and creds.get("api_key")) or None
170
+
171
+ if not api_key:
172
+ print("No API key found. Run `cyris login` first.", file=sys.stderr)
173
+ sys.exit(1)
174
+
175
+ api_url = api_url.rstrip("/")
176
+
177
+ try:
178
+ import requests
179
+
180
+ resp = requests.get(
181
+ f"{api_url}/agents",
182
+ headers={"Authorization": f"Bearer {api_key}"},
183
+ timeout=10,
184
+ )
185
+ if resp.status_code == 200:
186
+ data = resp.json()
187
+ agents = data if isinstance(data, list) else data.get("agents", [])
188
+ print(f"Connected to {api_url}")
189
+ print(f"Agents: {len(agents)}")
190
+ else:
191
+ print(f"API returned HTTP {resp.status_code}", file=sys.stderr)
192
+ sys.exit(1)
193
+ except ImportError:
194
+ print("The `requests` package is required. pip install requests", file=sys.stderr)
195
+ sys.exit(1)
196
+ except Exception as exc:
197
+ print(f"Connection failed: {exc}", file=sys.stderr)
198
+ sys.exit(1)
199
+
200
+
201
+ if __name__ == "__main__":
202
+ main()
@@ -0,0 +1,216 @@
1
+ """
2
+ Core initialisation and logging functions for the Cyris SDK.
3
+
4
+ This module exposes the four public helpers re-exported from ``cyris``:
5
+
6
+ cyris.init() -- configure the SDK
7
+ cyris.log_call() -- record an LLM invocation
8
+ cyris.log_override() -- record a human override
9
+ cyris.log_event() -- record an arbitrary audit event
10
+ """
11
+
12
+ import hashlib
13
+ import os
14
+ import sys
15
+ import time
16
+ import uuid
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ from . import buffer
20
+ from .credentials import load_credentials
21
+ from .fingerprint import generate_fingerprints
22
+ from .phi import detect_phi
23
+ from .session import get_current_session
24
+
25
+ # ------------------------------------------------------------------ #
26
+ # Module-level config (populated by ``init``) #
27
+ # ------------------------------------------------------------------ #
28
+
29
+ _config: Dict[str, Any] = {
30
+ "api_key": None,
31
+ "api_url": "https://cyrisai.dev/api/v1",
32
+ "agent_name": None,
33
+ "auto_patch": True,
34
+ "initialised": False,
35
+ }
36
+
37
+
38
+ def _resolve_api_key(api_key: Optional[str]) -> Optional[str]:
39
+ """Resolve the API key from (in order) parameter, env var, saved creds."""
40
+ if api_key:
41
+ return api_key
42
+
43
+ env_key = os.environ.get("CYRIS_API_KEY")
44
+ if env_key:
45
+ return env_key
46
+
47
+ creds = load_credentials()
48
+ if creds and creds.get("api_key"):
49
+ return creds["api_key"]
50
+
51
+ return None
52
+
53
+
54
+ # ------------------------------------------------------------------ #
55
+ # init #
56
+ # ------------------------------------------------------------------ #
57
+
58
+ def init(
59
+ api_key: Optional[str] = None,
60
+ api_url: Optional[str] = None,
61
+ agent_name: Optional[str] = None,
62
+ auto_patch: bool = True,
63
+ ) -> None:
64
+ """Initialise the Cyris SDK.
65
+
66
+ Parameters
67
+ ----------
68
+ api_key : str, optional
69
+ Cyris API key. Falls back to ``CYRIS_API_KEY`` env var, then to
70
+ saved credentials at ``~/.cyris/credentials``.
71
+ api_url : str, optional
72
+ Base URL of the Cyris API (default ``https://cyrisai.dev/api/v1``).
73
+ agent_name : str, optional
74
+ Logical name of the agent being instrumented.
75
+ auto_patch : bool
76
+ When ``True`` (default), monkey-patch OpenAI and Anthropic client
77
+ libraries so every call is automatically logged.
78
+ """
79
+ resolved_key = _resolve_api_key(api_key)
80
+ if not resolved_key:
81
+ try:
82
+ print(
83
+ "[cyris] WARNING: no API key found. "
84
+ "Pass api_key=, set CYRIS_API_KEY, or run `cyris login`.",
85
+ file=sys.stderr,
86
+ )
87
+ except Exception:
88
+ pass
89
+
90
+ _config["api_key"] = resolved_key
91
+ _config["api_url"] = (api_url or _config["api_url"]).rstrip("/")
92
+ _config["agent_name"] = agent_name
93
+ _config["auto_patch"] = auto_patch
94
+ _config["initialised"] = True
95
+
96
+ # Configure and start the background buffer.
97
+ if resolved_key:
98
+ buffer.configure(_config["api_url"], resolved_key)
99
+ buffer.start()
100
+
101
+ # Monkey-patch provider libraries when requested.
102
+ if auto_patch:
103
+ try:
104
+ from . import patchers
105
+ patchers.apply_patches()
106
+ except Exception as exc: # noqa: BLE001
107
+ try:
108
+ print(f"[cyris] auto-patch failed: {exc}", file=sys.stderr)
109
+ except Exception:
110
+ pass
111
+
112
+
113
+ # ------------------------------------------------------------------ #
114
+ # log_call #
115
+ # ------------------------------------------------------------------ #
116
+
117
+ def log_call(
118
+ provider: str = "unknown",
119
+ model: str = "unknown",
120
+ input_text: str = "",
121
+ output_text: str = "",
122
+ input_tokens: int = 0,
123
+ output_tokens: int = 0,
124
+ latency_ms: float = 0.0,
125
+ status: str = "success",
126
+ error_message: Optional[str] = None,
127
+ session_id: Optional[str] = None,
128
+ agent_name: Optional[str] = None,
129
+ ) -> None:
130
+ """Record a single LLM invocation.
131
+
132
+ Safe to call even before ``init()`` -- events will be silently
133
+ dropped if no API key is configured.
134
+ """
135
+ try:
136
+ input_hash = hashlib.sha256(input_text.encode("utf-8")).hexdigest() if input_text else None
137
+ output_hash = hashlib.sha256(output_text.encode("utf-8")).hexdigest() if output_text else None
138
+
139
+ phi_result = detect_phi(input_text + " " + output_text)
140
+ fingerprints = generate_fingerprints(output_text)
141
+
142
+ event = {
143
+ "event_type": "llm_call",
144
+ "id": str(uuid.uuid4()),
145
+ "timestamp": time.time(),
146
+ "provider": provider,
147
+ "model": model,
148
+ "input_tokens": input_tokens,
149
+ "output_tokens": output_tokens,
150
+ "latency_ms": latency_ms,
151
+ "status": status,
152
+ "error_message": error_message,
153
+ "input_hash": input_hash,
154
+ "output_hash": output_hash,
155
+ "phi_detected": phi_result["detected"],
156
+ "phi_fields": phi_result["fields_found"],
157
+ "fingerprints": fingerprints,
158
+ "session_id": session_id or get_current_session(),
159
+ "agent_name": agent_name or _config.get("agent_name"),
160
+ }
161
+
162
+ buffer.add_event(event)
163
+ except Exception: # noqa: BLE001
164
+ # NEVER crash the host application.
165
+ pass
166
+
167
+
168
+ # ------------------------------------------------------------------ #
169
+ # log_override #
170
+ # ------------------------------------------------------------------ #
171
+
172
+ def log_override(
173
+ invocation_id: str,
174
+ reason: str = "",
175
+ override_by: str = "",
176
+ ) -> None:
177
+ """Record a human override of an agent's decision."""
178
+ try:
179
+ event = {
180
+ "event_type": "override",
181
+ "id": str(uuid.uuid4()),
182
+ "timestamp": time.time(),
183
+ "invocation_id": invocation_id,
184
+ "reason": reason,
185
+ "override_by": override_by,
186
+ "session_id": get_current_session(),
187
+ "agent_name": _config.get("agent_name"),
188
+ }
189
+ buffer.add_event(event)
190
+ except Exception: # noqa: BLE001
191
+ pass
192
+
193
+
194
+ # ------------------------------------------------------------------ #
195
+ # log_event #
196
+ # ------------------------------------------------------------------ #
197
+
198
+ def log_event(
199
+ event_type: str = "custom",
200
+ description: str = "",
201
+ metadata: Optional[Dict[str, Any]] = None,
202
+ ) -> None:
203
+ """Record an arbitrary audit event."""
204
+ try:
205
+ event = {
206
+ "event_type": event_type,
207
+ "id": str(uuid.uuid4()),
208
+ "timestamp": time.time(),
209
+ "description": description,
210
+ "metadata": metadata or {},
211
+ "session_id": get_current_session(),
212
+ "agent_name": _config.get("agent_name"),
213
+ }
214
+ buffer.add_event(event)
215
+ except Exception: # noqa: BLE001
216
+ pass