feat: add human detection scoring and webhook notifications
Implement phase 2.1 (human detection) and 2.2 (notifications): - Detection scorer computes 0.0-1.0 human likelihood from keystroke timing variance, special key usage, typing speed, command diversity, and session duration - Webhook notifier sends JSON POST to configured endpoints with deduplication, custom headers, and event filtering - RecordingChannel gains an event callback for feeding keystrokes to the scorer without coupling shell and detection packages - Server wires scorer into session lifecycle with periodic updates and threshold-based notification triggers - Web UI shows human score in session tables with highlighting - New config sections: [detection] and [[notify.webhooks]] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
package shell
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RecordingChannel wraps an io.ReadWriteCloser. In Phase 1.4 it is a
|
||||
// pass-through; Phase 2.3 will add byte-level keystroke recording here
|
||||
// without changing any shell code.
|
||||
// EventCallback is called with a copy of data whenever the channel is read or written.
|
||||
// direction is 0 for input (client→server) and 1 for output (server→client).
|
||||
type EventCallback func(ts time.Time, direction int, data []byte)
|
||||
|
||||
// RecordingChannel wraps an io.ReadWriteCloser and optionally invokes a callback
|
||||
// on every Read (input) and Write (output). Phase 2.3 will add byte-level
|
||||
// keystroke recording here without changing any shell code.
|
||||
type RecordingChannel struct {
|
||||
inner io.ReadWriteCloser
|
||||
inner io.ReadWriteCloser
|
||||
callback EventCallback
|
||||
}
|
||||
|
||||
// NewRecordingChannel returns a RecordingChannel wrapping rw.
|
||||
@@ -14,6 +22,30 @@ func NewRecordingChannel(rw io.ReadWriteCloser) *RecordingChannel {
|
||||
return &RecordingChannel{inner: rw}
|
||||
}
|
||||
|
||||
func (r *RecordingChannel) Read(p []byte) (int, error) { return r.inner.Read(p) }
|
||||
func (r *RecordingChannel) Write(p []byte) (int, error) { return r.inner.Write(p) }
|
||||
func (r *RecordingChannel) Close() error { return r.inner.Close() }
|
||||
// WithCallback sets the event callback and returns the RecordingChannel for chaining.
|
||||
func (r *RecordingChannel) WithCallback(cb EventCallback) *RecordingChannel {
|
||||
r.callback = cb
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RecordingChannel) Read(p []byte) (int, error) {
|
||||
n, err := r.inner.Read(p)
|
||||
if n > 0 && r.callback != nil {
|
||||
cp := make([]byte, n)
|
||||
copy(cp, p[:n])
|
||||
r.callback(time.Now(), 0, cp)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *RecordingChannel) Write(p []byte) (int, error) {
|
||||
n, err := r.inner.Write(p)
|
||||
if n > 0 && r.callback != nil {
|
||||
cp := make([]byte, n)
|
||||
copy(cp, p[:n])
|
||||
r.callback(time.Now(), 1, cp)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *RecordingChannel) Close() error { return r.inner.Close() }
|
||||
|
||||
Reference in New Issue
Block a user