feat(audit): optional best-effort webhook for clipboard and file events - #256
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds an opt-in audit webhook channel that emits best-effort JSON metadata for clipboard and file-upload events.
Changes:
- Introduces
audit.pywith an async, fire-and-forgetAuditClientand module-levelconfigure/emithelpers. - Emits audit events on clipboard send/receive and on file-upload completion/error paths.
- Adds new settings and configures the audit client at startup.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/selkies/settings.py | Adds settings for audit webhook URL/token/timeout. |
| src/selkies/selkies.py | Emits audit events for clipboard send and file-upload end/error. |
| src/selkies/input_handler.py | Emits audit events when clipboard content is received (single- and multi-part). |
| src/selkies/audit.py | New module implementing the audit webhook emitter (aiohttp, fire-and-forget). |
| src/selkies/main.py | Configures the audit client from settings at startup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Code Review
This pull request introduces a best-effort, non-blocking audit-event emitter to log metadata for clipboard and file-transfer events via an optional webhook. Key feedback focuses on securing the sensitive webhook token by marking it as sensitive in settings, maintaining strong references to background asyncio tasks to prevent premature garbage collection, safely reading response bodies to avoid memory exhaustion, and offloading the blocking os.path.getsize call to a separate thread to prevent event loop lag.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
Thanks for the thorough review. Addressed:
Left Smoke-tested on the EL/XFCE (py3.9) image: default no-op, |
|
Heads-up for reviewers: the failing |
|
I think the decision is yet to be taken on this. |
|
Agreed, thanks for the pointer. File download is the nginx Happy to hold for the decision on whether to take this at all. |
|
Note that #254 removes the explicit NGINX dependency (it can still be sped up by NGINX). |
|
I'd like to defer this work until after v2.0.0; this is a non-critical component. |
Add an opt-in audit channel that POSTs small JSON metadata objects to a configurable webhook on clipboard send/receive and file upload events. Only metadata is sent (event type, byte size, mime type, timestamp); payload contents are never logged. The emitter is non-blocking and best-effort, so a slow or unreachable collector cannot stall the streaming pipeline. Disabled by default - an empty URL short-circuits emit() to a no-op. New settings: audit_webhook_url, audit_webhook_token, audit_webhook_timeout. Signed-off-by: DL6ER <dl6er@dl6er.de>
File download is served by the nginx fancyindex module, outside the Selkies Python package, so it cannot be observed from here. The emitter already only emits clipboard and file-upload events; drop the docstring claim that it also covers downloads. Signed-off-by: DL6ER <dl6er@dl6er.de>
57428e8 to
c986690
Compare
Reference the issue numbers and reviewers
Closes #249.
Explain relevant issues and how this pull request solves them
#249 asks for a way to observe the data-transfer channels Selkies handles - an audit trail for regulated environments, without forking Selkies. Inside the Selkies Python package that means the bidirectional clipboard and file upload; file download is served by the nginx
libnginx-mod-http-fancyindexmodule and is out of scope here.This pull request implements the webhook design proposed in #249: a single optional
audit_webhook_urlsetting. When unset (the default) Selkies behaves exactly as before. When set, Selkies POSTs a small JSON object to that URL on each clipboard / file-upload event, carrying metadata only - never the payload contents.Describe the changes in code and its dependencies and justify that they work as intended after testing
selkies/audit.py: a fire-and-forget JSON-over-HTTP sink. Owns one lazily-createdaiohttp.ClientSession, exposes module-levelconfigure()/emit()/is_enabled()/close().emit()schedules the POST on the running loop and returns immediately; all failures (DNS, refused, timeout, HTTP >= 400, anythingaiohttpraises) are caught and logged, so a slow or unreachable collector can never stall the streaming pipeline. When no URL is configured,emit()is a no-op.settings.py: three newstrsettings -audit_webhook_url,audit_webhook_token,audit_webhook_timeout(defaults keep the channel disabled).__main__.py:audit.configure(...)is called once at startup from the parsed settings.emit()call sites:clipboard.send(insend_ws_clipboard_data),clipboard.receive(text / binary / multipart paths in the input handler),file.upload.endandfile.upload.error. Each carries event id, RFC 3339 timestamp, byte size, mime type and/or filename as applicable.aiohttp, already a Selkies dependency.Testing: the module ships in our EL/XFCE (Python 3.9) build and is imported and configured on every session start; the default (no URL) no-op path is exercised by the end-to-end suite, which is green, confirming the additions do not affect existing behavior when the channel is disabled. The enabled path was exercised against a local collector:
configure()with a URL + Bearer token, thenemit()for clipboard and file events, produces one POST per event with the expected JSON metadata andAuthorization: Bearerheader; a down/refusing collector only logs a warning and does not interrupt the session.Describe alternatives you've considered
Additional context
The Bearer token is sent as
Authorization: Bearer ...specifically so the POST can be terminated at any standard reverse proxy with mature auth middleware, as outlined in #249.