Skip to content
Open
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/checkpoint/temporary.go
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,8 @@ func addDirectoryToEntriesWithAbsPath(repo *git.Repository, dirPathAbs, dirPathR

treePath := filepath.ToSlash(filepath.Join(dirPathRel, relWithinDir))

// Use redacted blob creation for metadata files (transcripts, prompts, etc.)
// to ensure PII and secrets are redacted before writing to git.
blobHash, mode, err := createRedactedBlobFromFile(repo, path, treePath)
if err != nil {
return fmt.Errorf("failed to create blob for %s: %w", path, err)
Expand Down
27 changes: 27 additions & 0 deletions cmd/entire/cli/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ type EntireSettings struct {
// Telemetry controls anonymous usage analytics.
// nil = not asked yet (show prompt), true = opted in, false = opted out
Telemetry *bool `json:"telemetry,omitempty"`

// Redaction configures PII redaction behavior for transcripts and metadata.
Redaction *RedactionSettings `json:"redaction,omitempty"`
}

// RedactionSettings configures redaction behavior beyond the default secret detection.
type RedactionSettings struct {
PII *PIISettings `json:"pii,omitempty"`
}

// PIISettings configures PII detection categories.
// When Enabled is true, email and phone default to true; address defaults to false.
type PIISettings struct {
Enabled bool `json:"enabled"`
Email *bool `json:"email,omitempty"`
Phone *bool `json:"phone,omitempty"`
Address *bool `json:"address,omitempty"`
CustomPatterns map[string]string `json:"custom_patterns,omitempty"`
}

// Load loads the Entire settings from .entire/settings.json,
Expand Down Expand Up @@ -204,6 +222,15 @@ func mergeJSON(settings *EntireSettings, data []byte) error {
settings.Telemetry = &t
}

// Override redaction if present
if redactionRaw, ok := raw["redaction"]; ok {
var r RedactionSettings
if err := json.Unmarshal(redactionRaw, &r); err != nil {
return fmt.Errorf("parsing redaction field: %w", err)
}
settings.Redaction = &r
}

return nil
}

Expand Down
86 changes: 85 additions & 1 deletion cmd/entire/cli/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ func TestLoad_AcceptsValidKeys(t *testing.T) {
"local_dev": false,
"log_level": "debug",
"strategy_options": {"key": "value"},
"telemetry": true
"telemetry": true,
"redaction": {"pii": {"enabled": true, "email": true, "phone": false}}
}`
if err := os.WriteFile(settingsFile, []byte(settingsContent), 0644); err != nil {
t.Fatalf("failed to write settings file: %v", err)
Expand Down Expand Up @@ -92,6 +93,21 @@ func TestLoad_AcceptsValidKeys(t *testing.T) {
if settings.Telemetry == nil || !*settings.Telemetry {
t.Error("expected telemetry to be true")
}
if settings.Redaction == nil {
t.Fatal("expected redaction to be non-nil")
}
if settings.Redaction.PII == nil {
t.Fatal("expected redaction.pii to be non-nil")
}
if !settings.Redaction.PII.Enabled {
t.Error("expected redaction.pii.enabled to be true")
}
if settings.Redaction.PII.Email == nil || !*settings.Redaction.PII.Email {
t.Error("expected redaction.pii.email to be true")
}
if settings.Redaction.PII.Phone == nil || *settings.Redaction.PII.Phone {
t.Error("expected redaction.pii.phone to be false")
}
}

func TestLoad_LocalSettingsRejectsUnknownKeys(t *testing.T) {
Expand Down Expand Up @@ -135,6 +151,74 @@ func TestLoad_LocalSettingsRejectsUnknownKeys(t *testing.T) {
}
}

func TestLoad_MissingRedactionIsNil(t *testing.T) {
tmpDir := t.TempDir()
entireDir := filepath.Join(tmpDir, ".entire")
if err := os.MkdirAll(entireDir, 0755); err != nil {
t.Fatalf("failed to create .entire directory: %v", err)
}

settingsFile := filepath.Join(entireDir, "settings.json")
if err := os.WriteFile(settingsFile, []byte(`{"strategy": "manual-commit"}`), 0644); err != nil {
t.Fatalf("failed to write settings file: %v", err)
}
if err := os.MkdirAll(filepath.Join(tmpDir, ".git"), 0755); err != nil {
t.Fatalf("failed to create .git directory: %v", err)
}
t.Chdir(tmpDir)

settings, err := Load()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if settings.Redaction != nil {
t.Error("expected redaction to be nil when not in settings")
}
}

func TestLoad_LocalOverridesRedaction(t *testing.T) {
tmpDir := t.TempDir()
entireDir := filepath.Join(tmpDir, ".entire")
if err := os.MkdirAll(entireDir, 0755); err != nil {
t.Fatalf("failed to create .entire directory: %v", err)
}

// Base settings: PII disabled
settingsFile := filepath.Join(entireDir, "settings.json")
if err := os.WriteFile(settingsFile, []byte(`{"strategy": "manual-commit", "redaction": {"pii": {"enabled": false}}}`), 0644); err != nil {
t.Fatalf("failed to write settings file: %v", err)
}

// Local override: PII enabled with custom patterns
localFile := filepath.Join(entireDir, "settings.local.json")
localContent := `{"redaction": {"pii": {"enabled": true, "custom_patterns": {"employee_id": "EMP-\\d{6}"}}}}`
if err := os.WriteFile(localFile, []byte(localContent), 0644); err != nil {
t.Fatalf("failed to write local settings file: %v", err)
}

if err := os.MkdirAll(filepath.Join(tmpDir, ".git"), 0755); err != nil {
t.Fatalf("failed to create .git directory: %v", err)
}
t.Chdir(tmpDir)

settings, err := Load()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if settings.Redaction == nil || settings.Redaction.PII == nil {
t.Fatal("expected redaction.pii to be non-nil after local override")
}
if !settings.Redaction.PII.Enabled {
t.Error("expected local override to enable PII")
}
if settings.Redaction.PII.CustomPatterns == nil {
t.Fatal("expected custom_patterns to be non-nil")
}
if settings.Redaction.PII.CustomPatterns["employee_id"] != `EMP-\d{6}` {
t.Errorf("expected employee_id pattern, got %v", settings.Redaction.PII.CustomPatterns)
}
}

// containsUnknownField checks if the error message indicates an unknown field
func containsUnknownField(msg string) bool {
// Go's json package reports unknown fields with this message format
Expand Down
3 changes: 3 additions & 0 deletions cmd/entire/cli/strategy/auto_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ func (s *AutoCommitStrategy) PrePush(remote string) error {
}

func (s *AutoCommitStrategy) SaveStep(ctx StepContext) error {
EnsureRedactionConfigured()

repo, err := OpenRepository()
if err != nil {
return fmt.Errorf("failed to open git repository: %w", err)
Expand Down Expand Up @@ -501,6 +503,7 @@ func (s *AutoCommitStrategy) GetSessionInfo() (*SessionInfo, error) {
// 1. Commit code changes to active branch (no trailers - clean history)
// 2. Commit task metadata to entire/checkpoints/v1 branch with checkpoint format
func (s *AutoCommitStrategy) SaveTaskStep(ctx TaskStepContext) error {
EnsureRedactionConfigured()
repo, err := OpenRepository()
if err != nil {
return fmt.Errorf("failed to open git repository: %w", err)
Expand Down
31 changes: 31 additions & 0 deletions cmd/entire/cli/strategy/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import (
"github.com/entireio/cli/cmd/entire/cli/checkpoint"
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/settings"
"github.com/entireio/cli/cmd/entire/cli/trailers"
"github.com/entireio/cli/redact"

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
Expand Down Expand Up @@ -238,6 +240,35 @@ var (
protectedDirsCache []string
)

var initRedactionOnce sync.Once

// EnsureRedactionConfigured loads PII redaction settings and configures the
// redact package. Called once before any checkpoint writes. No-op if PII is
// not enabled in settings.
func EnsureRedactionConfigured() {
initRedactionOnce.Do(func() {
s, err := settings.Load()
if err != nil {
fmt.Fprintf(os.Stderr, "[entire] Warning: failed to load settings for PII redaction: %v\n", err)
return
}
if s.Redaction == nil || s.Redaction.PII == nil || !s.Redaction.PII.Enabled {
return
}
pii := s.Redaction.PII
cfg := redact.PIIConfig{
Enabled: true,
Categories: make(map[redact.PIICategory]bool),
CustomPatterns: pii.CustomPatterns,
}
// Email and phone default to true when PII is enabled; address defaults to false.
cfg.Categories[redact.PIIEmail] = pii.Email == nil || *pii.Email
cfg.Categories[redact.PIIPhone] = pii.Phone == nil || *pii.Phone
cfg.Categories[redact.PIIAddress] = pii.Address != nil && *pii.Address
redact.ConfigurePII(cfg)
})
}

// homeRelativePath strips the $HOME/ prefix from an absolute path,
// returning a home-relative path suitable for persisting in metadata.
// Returns "" if the path is empty or not under $HOME.
Expand Down
1 change: 1 addition & 0 deletions cmd/entire/cli/strategy/manual_commit_condensation.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ func generateContextFromPrompts(prompts []string) []byte {
// CondenseSessionByID force-condenses a session by its ID and cleans up.
// This is used by "entire doctor" to salvage stuck sessions.
func (s *ManualCommitStrategy) CondenseSessionByID(sessionID string) error {
EnsureRedactionConfigured()
ctx := logging.WithComponent(context.Background(), "condense-by-id")

// Load session state
Expand Down
3 changes: 3 additions & 0 deletions cmd/entire/cli/strategy/manual_commit_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
// SaveStep saves a checkpoint to the shadow branch.
// Uses checkpoint.GitStore.WriteTemporary for git operations.
func (s *ManualCommitStrategy) SaveStep(ctx StepContext) error {
EnsureRedactionConfigured()

repo, err := OpenRepository()
if err != nil {
return fmt.Errorf("failed to open git repository: %w", err)
Expand Down Expand Up @@ -166,6 +168,7 @@ func (s *ManualCommitStrategy) SaveStep(ctx StepContext) error {
// SaveTaskStep saves a task step checkpoint to the shadow branch.
// Uses checkpoint.GitStore.WriteTemporaryTask for git operations.
func (s *ManualCommitStrategy) SaveTaskStep(ctx TaskStepContext) error {
EnsureRedactionConfigured()
repo, err := OpenRepository()
if err != nil {
return fmt.Errorf("failed to open git repository: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions cmd/entire/cli/strategy/manual_commit_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,8 @@ func (h *postCommitActionHandler) HandleWarnStaleSession(_ *session.State) error
//
//nolint:unparam // error return required by interface but hooks must return nil
func (s *ManualCommitStrategy) PostCommit() error {
EnsureRedactionConfigured()

logCtx := logging.WithComponent(context.Background(), "checkpoint")

repo, err := OpenRepository()
Expand Down
Loading