This repository has been archived on 2026-03-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
oubliette/internal/shell/recorder.go
Torjus Håkestad 0ad6f4cb6a 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>
2026-02-14 21:28:11 +01:00

52 lines
1.4 KiB
Go

package shell
import (
"io"
"time"
)
// 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
callback EventCallback
}
// NewRecordingChannel returns a RecordingChannel wrapping rw.
func NewRecordingChannel(rw io.ReadWriteCloser) *RecordingChannel {
return &RecordingChannel{inner: rw}
}
// 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() }