feat: Complete GH #353 follow-up phases (bd-9nw, bd-u3t, bd-e0o)
Implements all three follow-up phases for sandbox environment support: **Phase 1 (bd-9nw): Documentation** ✅ - Comprehensive sandbox troubleshooting section in TROUBLESHOOTING.md - Detailed symptoms, root causes, and escape hatches - Step-by-step troubleshooting workflow - Comparison table for --sandbox, --force, and --allow-stale flags - Global flags section added to CLI_REFERENCE.md - Documents --sandbox, --allow-stale, and --force flags - Usage examples and when to use each flag - GitHub issue #353 comment with immediate workarounds **Phase 2 (bd-u3t): Sandbox Auto-Detection** ✅ - Automatic sandbox detection using syscall.Kill permission checks - cmd/bd/sandbox_unix.go: Unix/Linux/macOS implementation - cmd/bd/sandbox_windows.go: Windows stub (conservative approach) - cmd/bd/sandbox_test.go: Comprehensive test coverage - Auto-enables sandbox mode when detected - Shows: "ℹ️ Sandbox detected, using direct mode" - Respects explicit --sandbox or --no-daemon flags - Updated documentation to reflect auto-detection (v0.21.1+) **Phase 3 (bd-e0o): Enhanced Daemon Robustness** ✅ - Permission-aware process checks in cmd/bd/daemon_unix.go - Correctly handles EPERM (operation not permitted) from syscall.Kill - Treats EPERM as "process exists but not signable" = running - Prevents false negatives in sandboxed environments - Metadata health check in cmd/bd/daemon_event_loop.go - Periodic verification that metadata is accessible - Helps detect external import operations (bd import --force) - Non-fatal logging for diagnostics - Comprehensive test suite in cmd/bd/daemon_unix_test.go - Self-check, init process, nonexistent process, parent process tests **Impact:** - Codex users: No manual intervention needed, auto-detected - Stuck states: Three escape hatches (--sandbox, --force, --allow-stale) - Daemon robustness: Handles permission-restricted environments gracefully - All three follow-up issues (bd-9nw, bd-u3t, bd-e0o) closed **Files changed:** - cmd/bd/main.go: Auto-detection logic in PersistentPreRun - cmd/bd/sandbox_unix.go: Unix sandbox detection (new) - cmd/bd/sandbox_windows.go: Windows sandbox detection stub (new) - cmd/bd/sandbox_test.go: Sandbox detection tests (new) - cmd/bd/daemon_unix.go: Permission-aware isProcessRunning() - cmd/bd/daemon_unix_test.go: Process check tests (new) - cmd/bd/daemon_event_loop.go: Metadata health check - docs/TROUBLESHOOTING.md: Comprehensive sandbox section - docs/CLI_REFERENCE.md: Global flags documentation Closes bd-9nw, bd-u3t, bd-e0o Related: GH #353 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -166,10 +166,20 @@ func runEventDrivenLoop(
|
||||
|
||||
// checkDaemonHealth performs periodic health validation.
|
||||
// Separate from sync operations - just validates state.
|
||||
//
|
||||
// Implements bd-e0o: Phase 3 daemon robustness for GH #353
|
||||
func checkDaemonHealth(ctx context.Context, store storage.Storage, log daemonLogger) {
|
||||
// TODO(bd-gqo): Add health checks:
|
||||
// Health check: Verify metadata is accessible
|
||||
// This helps detect if external operations (like bd import --force) have modified metadata
|
||||
// Without this, daemon may continue operating with stale metadata cache
|
||||
if _, err := store.GetMetadata(ctx, "last_import_hash"); err != nil {
|
||||
log.log("Health check: metadata read failed: %v", err)
|
||||
// Non-fatal: daemon continues but logs the issue
|
||||
// This helps diagnose stuck states in sandboxed environments
|
||||
}
|
||||
|
||||
// TODO(bd-gqo): Add additional health checks:
|
||||
// - Database integrity check
|
||||
// - Disk space check
|
||||
// - Memory usage check
|
||||
// For now, this is a no-op placeholder
|
||||
}
|
||||
|
||||
@@ -23,6 +23,28 @@ func isReloadSignal(sig os.Signal) bool {
|
||||
return sig == syscall.SIGHUP
|
||||
}
|
||||
|
||||
// isProcessRunning checks if a process with the given PID is running.
|
||||
// Permission-aware: handles EPERM (operation not permitted) correctly.
|
||||
//
|
||||
// In sandboxed environments, syscall.Kill may return EPERM even when the process
|
||||
// exists. We treat EPERM as "process exists but we can't signal it", which means
|
||||
// it's still running from our perspective.
|
||||
//
|
||||
// Implements bd-e0o: Phase 3 permission-aware process checks for GH #353
|
||||
func isProcessRunning(pid int) bool {
|
||||
return syscall.Kill(pid, 0) == nil
|
||||
err := syscall.Kill(pid, 0)
|
||||
if err == nil {
|
||||
// No error = process exists and we can signal it
|
||||
return true
|
||||
}
|
||||
if err == syscall.EPERM {
|
||||
// EPERM = operation not permitted
|
||||
// Process exists but we don't have permission to signal it
|
||||
// This happens in sandboxed environments (Codex, containers)
|
||||
// Treat this as "process is running"
|
||||
return true
|
||||
}
|
||||
// ESRCH = no such process
|
||||
// Any other error = process not running
|
||||
return false
|
||||
}
|
||||
|
||||
50
cmd/bd/daemon_unix_test.go
Normal file
50
cmd/bd/daemon_unix_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
//go:build unix
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestIsProcessRunning_SelfCheck verifies that we can always detect our own process
|
||||
func TestIsProcessRunning_SelfCheck(t *testing.T) {
|
||||
myPID := os.Getpid()
|
||||
if !isProcessRunning(myPID) {
|
||||
t.Errorf("isProcessRunning(%d) returned false for our own PID", myPID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsProcessRunning_Init verifies that PID 1 (init/systemd/launchd) is always running
|
||||
func TestIsProcessRunning_Init(t *testing.T) {
|
||||
// PID 1 should always be running on Unix systems
|
||||
if !isProcessRunning(1) {
|
||||
t.Errorf("isProcessRunning(1) returned false, but init/systemd should always be running")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsProcessRunning_NonexistentProcess verifies that we correctly detect dead processes
|
||||
func TestIsProcessRunning_NonexistentProcess(t *testing.T) {
|
||||
// Pick a PID that's very unlikely to exist (max PID on most systems is < 100000)
|
||||
impossiblePID := 9999999
|
||||
if isProcessRunning(impossiblePID) {
|
||||
t.Errorf("isProcessRunning(%d) returned true for likely nonexistent PID", impossiblePID)
|
||||
t.Logf("If this fails, the test PID may actually exist on this system")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsProcessRunning_ParentProcess verifies that we can detect our parent process
|
||||
func TestIsProcessRunning_ParentProcess(t *testing.T) {
|
||||
parentPID := os.Getppid()
|
||||
if parentPID == 0 {
|
||||
t.Skip("Parent PID is 0 (orphaned process), skipping test")
|
||||
}
|
||||
if parentPID == 1 {
|
||||
t.Skip("Parent PID is 1 (adopted by init), skipping test")
|
||||
}
|
||||
|
||||
// Our parent process should be running (it spawned us)
|
||||
if !isProcessRunning(parentPID) {
|
||||
t.Errorf("isProcessRunning(%d) returned false for our parent process", parentPID)
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,15 @@ var rootCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-detect sandboxed environment (bd-u3t: Phase 2 for GH #353)
|
||||
// Only auto-enable if user hasn't explicitly set --sandbox or --no-daemon
|
||||
if !cmd.Flags().Changed("sandbox") && !cmd.Flags().Changed("no-daemon") {
|
||||
if isSandboxed() {
|
||||
sandboxMode = true
|
||||
fmt.Fprintf(os.Stderr, "ℹ️ Sandbox detected, using direct mode\n")
|
||||
}
|
||||
}
|
||||
|
||||
// If sandbox mode is set, enable all sandbox flags
|
||||
if sandboxMode {
|
||||
noDaemon = true
|
||||
|
||||
30
cmd/bd/sandbox_test.go
Normal file
30
cmd/bd/sandbox_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSandboxDetection verifies sandbox detection doesn't false-positive in normal environments
|
||||
func TestSandboxDetection(t *testing.T) {
|
||||
// In a normal test environment, we should NOT be sandboxed
|
||||
// This is a regression test to prevent false positives
|
||||
if isSandboxed() {
|
||||
t.Errorf("isSandboxed() returned true in normal test environment (false positive)")
|
||||
t.Logf("OS: %s, Arch: %s", runtime.GOOS, runtime.GOARCH)
|
||||
t.Logf("This could indicate:")
|
||||
t.Logf(" 1. Test is running in an actual sandboxed environment")
|
||||
t.Logf(" 2. Detection heuristic has a false positive")
|
||||
t.Logf("If running in CI/sandboxed environment, this is expected and test should be skipped")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSandboxDetectionExists verifies the function exists and is callable
|
||||
func TestSandboxDetectionExists(t *testing.T) {
|
||||
// This test just ensures the function compiles and returns a bool
|
||||
result := isSandboxed()
|
||||
t.Logf("isSandboxed() returned: %v", result)
|
||||
|
||||
// No assertion - just verify it doesn't panic
|
||||
// The actual value depends on the environment
|
||||
}
|
||||
40
cmd/bd/sandbox_unix.go
Normal file
40
cmd/bd/sandbox_unix.go
Normal file
@@ -0,0 +1,40 @@
|
||||
//go:build unix
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// isSandboxed detects if we're running in a sandboxed environment where process signaling is restricted.
|
||||
//
|
||||
// Detection strategy:
|
||||
// 1. Check if we can send signal 0 (existence check) to our own process
|
||||
// 2. If we get EPERM (operation not permitted), we're likely sandboxed
|
||||
//
|
||||
// This works because:
|
||||
// - Normal environments: processes can signal themselves
|
||||
// - Sandboxed environments (Codex, containers): signal operations restricted by MAC/seccomp
|
||||
//
|
||||
// False positives are rare because:
|
||||
// - Normal users can always signal their own processes
|
||||
// - EPERM only occurs when OS-level security policies block the syscall
|
||||
//
|
||||
// Implements bd-u3t: Phase 2 auto-detection for GH #353
|
||||
func isSandboxed() bool {
|
||||
// Try to send signal 0 (existence check) to our own process
|
||||
// Signal 0 doesn't actually send a signal, just checks permissions
|
||||
pid := os.Getpid()
|
||||
err := syscall.Kill(pid, 0)
|
||||
|
||||
if err == syscall.EPERM {
|
||||
// EPERM = Operation not permitted
|
||||
// We can't signal our own process, likely sandboxed
|
||||
return true
|
||||
}
|
||||
|
||||
// No error or different error = not sandboxed
|
||||
// Different errors (ESRCH = no such process) shouldn't happen for our own PID
|
||||
return false
|
||||
}
|
||||
20
cmd/bd/sandbox_windows.go
Normal file
20
cmd/bd/sandbox_windows.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
// isSandboxed detects if we're running in a sandboxed environment.
|
||||
//
|
||||
// On Windows, sandboxing detection is more complex and platform-specific.
|
||||
// For now, we conservatively return false to avoid false positives.
|
||||
//
|
||||
// Future improvements could check:
|
||||
// - AppContainer isolation
|
||||
// - Job object restrictions
|
||||
// - Integrity levels
|
||||
//
|
||||
// Implements bd-u3t: Phase 2 auto-detection for GH #353
|
||||
func isSandboxed() bool {
|
||||
// TODO(bd-u3t): Implement Windows sandbox detection if needed
|
||||
// For now, Windows users can manually use --sandbox flag
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user