Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/entire/cli/dispatch_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type dispatchProgram interface {
Run() (tea.Model, error)
}

// newDispatchProgram is overridden by tests via assignment. Tests that mutate
// it cannot use t.Parallel() — they would race each other's factory.
var newDispatchProgram = func(model tea.Model, outW io.Writer, altScreen bool) dispatchProgram {
options := []tea.ProgramOption{tea.WithOutput(outW)}
if altScreen {
Expand Down
4 changes: 0 additions & 4 deletions cmd/entire/cli/dispatch_tui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ func (p fakeDispatchProgram) Run() (tea.Model, error) {
}

func TestDefaultRunInteractiveDispatch_DoesNotUseAltScreen(t *testing.T) {
t.Parallel()

oldProgramFactory := newDispatchProgram
newDispatchProgram = func(model tea.Model, _ io.Writer, altScreen bool) dispatchProgram {
if altScreen {
Expand Down Expand Up @@ -73,8 +71,6 @@ func TestDispatchStatusModel_ViewRendersInlineCard(t *testing.T) {
}

func TestDefaultRunInteractiveDispatch_ClearsLoadingCardBeforeReturn(t *testing.T) {
t.Parallel()

oldProgramFactory := newDispatchProgram
newDispatchProgram = func(model tea.Model, _ io.Writer, _ bool) dispatchProgram {
return fakeDispatchProgram{model: model}
Expand Down
32 changes: 11 additions & 21 deletions cmd/entire/cli/logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,25 +174,6 @@ func resetLogger() {
}
}

// getLogger returns the current logger, or a default stderr logger if not initialized.
func getLogger() *slog.Logger {
mu.RLock()
defer mu.RUnlock()

if logger == nil {
// Return default stderr logger
return slog.Default()
}
return logger
}

// getSessionID returns the current session ID (thread-safe).
func getSessionID() string {
mu.RLock()
defer mu.RUnlock()
return currentSessionID
}

// createLogger creates a JSON logger writing to the given writer at the specified level.
func createLogger(w io.Writer, level slog.Level) *slog.Logger {
opts := &slog.HandlerOptions{
Expand Down Expand Up @@ -273,14 +254,23 @@ func LogDuration(ctx context.Context, level slog.Level, msg string, start time.T
}

// log is the internal logging function that extracts context values and logs.
//
// The read lock is held across l.Log so Init/Close cannot close logBufWriter
// mid-write; do not shrink the lock scope to a snapshot pattern.
func log(ctx context.Context, level slog.Level, msg string, attrs ...any) {
l := getLogger()
mu.RLock()
defer mu.RUnlock()

l := logger
if l == nil {
l = slog.Default()
}
globalSessionID := currentSessionID

// Build attributes slice with session ID first (if set)
var allAttrs []any

// Add session ID from Init() if set (always first for consistency)
globalSessionID := getSessionID()
if globalSessionID != "" {
allAttrs = append(allAttrs, slog.String("session_id", globalSessionID))
}
Expand Down
61 changes: 61 additions & 0 deletions cmd/entire/cli/logging/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
Expand Down Expand Up @@ -536,6 +537,66 @@ func TestLogging_ContextSessionID_WhenNoGlobalSet(t *testing.T) {
resetLogger()
}

func TestLogging_ConcurrentInitAndLog(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
initGitRepo(t, tmpDir)

if err := Init(context.Background(), ""); err != nil {
t.Fatalf("Init() error = %v", err)
}
defer Close()

const (
logGoroutines = 8
initGoroutines = 4
closeGoroutines = 2
iterations = 200
)

var wg sync.WaitGroup
start := make(chan struct{})

for i := range logGoroutines {
wg.Add(1)
go func(worker int) {
defer wg.Done()
<-start
for j := range iterations {
Info(context.Background(), "concurrent log", slog.Int("worker", worker), slog.Int("iteration", j))
}
}(i)
}

for range initGoroutines {
wg.Add(1)
go func() {
defer wg.Done()
<-start
for range iterations {
if err := Init(context.Background(), ""); err != nil {
t.Errorf("Init() error = %v", err)
return
}
}
}()
}

for range closeGoroutines {
wg.Add(1)
go func() {
defer wg.Done()
<-start
for range iterations {
Close()
}
}()
}

close(start)
wg.Wait()
}

func TestInit_RejectsInvalidSessionIDs(t *testing.T) {
tests := []struct {
name string
Expand Down
9 changes: 7 additions & 2 deletions docs/security-and-privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ If your repository is **public**, this data is visible to the entire internet.

### What Entire redacts automatically

Entire automatically scans transcript and metadata content before writing it to the `entire/checkpoints/v1` branch. Two detection methods run during condensation:
Entire automatically scans transcript and metadata content before writing it to the `entire/checkpoints/v1` branch. Five secret detection methods run during condensation:

1. **Entropy scoring** — Identifies high-entropy strings (Shannon entropy > 4.5) that look like randomly generated secrets, even if they don't match a known pattern.
2. **Pattern matching** — Uses [Betterleaks](https://github.com/betterleaks/betterleaks) built-in rules to detect known secret formats.
3. **Credentialed URI detection** — Redacts URLs with embedded passwords, such as `scheme://user:password@host`.
4. **Database connection-string detection** — Redacts JDBC, Postgres keyword DSN, SQL Server, and ODBC-style connection strings containing passwords.
5. **Bounded credential value detection** — Redacts password-like config values such as `DB_PASSWORD=...` and `PGPASSWORD=...` while preserving the surrounding key.

Detected secrets are replaced with `REDACTED` before the data is ever written to a git object. This is **always on** and cannot be disabled.

Expand All @@ -35,10 +38,12 @@ If your AI sessions will touch sensitive data:

### Secrets (always on)

Betterleaks pattern matching covers cloud providers (AWS, GCP, Azure), version control platforms (GitHub, GitLab, Bitbucket), payment processors (Stripe, Square), communication tools (Slack, Discord, Twilio), private key blocks (RSA, DSA, EC, PGP), and generic credentials (bearer tokens, basic auth, JWTs). Entire also redacts database connection strings and other credentialed URLs containing `://user:password@host`. Entropy scoring catches secrets that don't match any known pattern.
Betterleaks pattern matching covers cloud providers (AWS, GCP, Azure), version control platforms (GitHub, GitLab, Bitbucket), payment processors (Stripe, Square), communication tools (Slack, Discord, Twilio), private key blocks (RSA, DSA, EC, PGP), and generic credentials (bearer tokens, basic auth, JWTs). Dedicated credentialed URI detection covers URLs that embed passwords. Additional database connection-string detection covers DB DSNs and query-parameter passwords not reliably covered by generic secret rules. Entropy scoring catches secrets that don't match any known pattern.

All detected secrets are replaced with `REDACTED`.

To reduce over-redaction, Entire preserves structural transcript fields such as IDs and paths, ignores common placeholder values, and redacts only credential values for bounded key/value forms. When a connection string contains a real (non-placeholder) password, it is redacted as a unit because partial fragments can still expose sensitive material; connection strings whose passwords are placeholders (e.g. `${DB_PASSWORD}`) are left intact.

## Limitations

- **Best-effort.** Novel or low-entropy secrets (short passwords, predictable tokens) may not be caught.
Expand Down
Loading
Loading