Merge branch 'main' of https://github.com/steveyegge/beads
# Conflicts: # .beads/beads.jsonl
This commit is contained in:
@@ -166,10 +166,20 @@ func runEventDrivenLoop(
|
|||||||
|
|
||||||
// checkDaemonHealth performs periodic health validation.
|
// checkDaemonHealth performs periodic health validation.
|
||||||
// Separate from sync operations - just validates state.
|
// 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) {
|
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
|
// - Database integrity check
|
||||||
// - Disk space check
|
// - Disk space check
|
||||||
// - Memory usage 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
|
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 {
|
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
|
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 sandbox mode is set, enable all sandbox flags
|
||||||
if sandboxMode {
|
if sandboxMode {
|
||||||
noDaemon = true
|
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
|
||||||
|
}
|
||||||
@@ -203,6 +203,83 @@ bd list --priority-min 2 --json # P2 and below
|
|||||||
bd list --status open --priority 1 --label-any urgent,critical --no-assignee --json
|
bd list --status open --priority 1 --label-any urgent,critical --no-assignee --json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Global Flags
|
||||||
|
|
||||||
|
Global flags work with any bd command and must appear **before** the subcommand.
|
||||||
|
|
||||||
|
### Sandbox Mode
|
||||||
|
|
||||||
|
**Auto-detection (v0.21.1+):** bd automatically detects sandboxed environments and enables sandbox mode.
|
||||||
|
|
||||||
|
When detected, you'll see: `ℹ️ Sandbox detected, using direct mode`
|
||||||
|
|
||||||
|
**Manual override:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Explicitly enable sandbox mode
|
||||||
|
bd --sandbox <command>
|
||||||
|
|
||||||
|
# Equivalent to combining these flags:
|
||||||
|
bd --no-daemon --no-auto-flush --no-auto-import <command>
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- Disables daemon (uses direct SQLite mode)
|
||||||
|
- Disables auto-export to JSONL
|
||||||
|
- Disables auto-import from JSONL
|
||||||
|
|
||||||
|
**When to use:** Sandboxed environments where daemon can't be controlled (permission restrictions), or when auto-detection doesn't trigger.
|
||||||
|
|
||||||
|
### Staleness Control
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Skip staleness check (emergency escape hatch)
|
||||||
|
bd --allow-stale <command>
|
||||||
|
|
||||||
|
# Example: access database even if out of sync with JSONL
|
||||||
|
bd --allow-stale ready --json
|
||||||
|
bd --allow-stale list --status open --json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Shows:** `⚠️ Staleness check skipped (--allow-stale), data may be out of sync`
|
||||||
|
|
||||||
|
**⚠️ Caution:** May show stale or incomplete data. Use only when stuck and other options fail.
|
||||||
|
|
||||||
|
### Force Import
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Force metadata update even when DB appears synced
|
||||||
|
bd import --force -i .beads/beads.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use:** `bd import` reports "0 created, 0 updated" but staleness errors persist.
|
||||||
|
|
||||||
|
**Shows:** `Metadata updated (database already in sync with JSONL)`
|
||||||
|
|
||||||
|
### Other Global Flags
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JSON output for programmatic use
|
||||||
|
bd --json <command>
|
||||||
|
|
||||||
|
# Force direct mode (bypass daemon)
|
||||||
|
bd --no-daemon <command>
|
||||||
|
|
||||||
|
# Disable auto-sync
|
||||||
|
bd --no-auto-flush <command> # Disable auto-export to JSONL
|
||||||
|
bd --no-auto-import <command> # Disable auto-import from JSONL
|
||||||
|
|
||||||
|
# Custom database path
|
||||||
|
bd --db /path/to/.beads/beads.db <command>
|
||||||
|
|
||||||
|
# Custom actor for audit trail
|
||||||
|
bd --actor alice <command>
|
||||||
|
```
|
||||||
|
|
||||||
|
**See also:**
|
||||||
|
- [TROUBLESHOOTING.md - Sandboxed environments](TROUBLESHOOTING.md#sandboxed-environments-codex-claude-code-etc) for detailed sandbox troubleshooting
|
||||||
|
- [DAEMON.md](DAEMON.md) for daemon mode details
|
||||||
|
|
||||||
## Advanced Operations
|
## Advanced Operations
|
||||||
|
|
||||||
### Cleanup
|
### Cleanup
|
||||||
|
|||||||
@@ -436,19 +436,36 @@ ps aux | grep "bd daemon"
|
|||||||
|
|
||||||
See [integrations/beads-mcp/README.md](integrations/beads-mcp/README.md) for MCP-specific troubleshooting.
|
See [integrations/beads-mcp/README.md](integrations/beads-mcp/README.md) for MCP-specific troubleshooting.
|
||||||
|
|
||||||
### Claude Code sandbox mode
|
### Sandboxed environments (Codex, Claude Code, etc.)
|
||||||
|
|
||||||
**Issue:** Claude Code's sandbox restricts network access to a single socket, conflicting with bd's daemon and git operations.
|
**Issue:** Sandboxed environments restrict permissions, preventing daemon control and causing "out of sync" errors.
|
||||||
|
|
||||||
**Solution:** Use the `--sandbox` flag:
|
**Common symptoms:**
|
||||||
|
- "Database out of sync with JSONL" errors that persist after running `bd import`
|
||||||
|
- `bd daemon --stop` fails with "operation not permitted"
|
||||||
|
- Cannot kill daemon process with `kill <pid>`
|
||||||
|
- JSONL hash mismatch warnings (bd-160)
|
||||||
|
- Commands intermittently fail with staleness errors
|
||||||
|
|
||||||
|
**Root cause:** The sandbox can't signal/kill the existing daemon process, so the DB stays stale and refuses to import.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Quick fix: Sandbox mode (auto-detected)
|
||||||
|
|
||||||
|
**As of v0.21.1+**, bd automatically detects sandboxed environments and enables sandbox mode.
|
||||||
|
|
||||||
|
When auto-detected, you'll see: `ℹ️ Sandbox detected, using direct mode`
|
||||||
|
|
||||||
|
**Manual override** (if auto-detection fails):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Sandbox mode disables daemon and auto-sync
|
# Explicitly enable sandbox mode
|
||||||
bd --sandbox ready
|
bd --sandbox ready
|
||||||
bd --sandbox create "Fix bug" -p 1
|
bd --sandbox create "Fix bug" -p 1
|
||||||
bd --sandbox update bd-42 --status in_progress
|
bd --sandbox update bd-42 --status in_progress
|
||||||
|
|
||||||
# Or set individual flags
|
# Equivalent to:
|
||||||
bd --no-daemon --no-auto-flush --no-auto-import <command>
|
bd --no-daemon --no-auto-flush --no-auto-import <command>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -464,7 +481,83 @@ bd --no-daemon --no-auto-flush --no-auto-import <command>
|
|||||||
bd sync
|
bd sync
|
||||||
```
|
```
|
||||||
|
|
||||||
**Related:** See [Claude Code sandboxing documentation](https://www.anthropic.com/engineering/claude-code-sandboxing) for more about sandbox restrictions.
|
---
|
||||||
|
|
||||||
|
#### Escape hatches for stuck states
|
||||||
|
|
||||||
|
If you're stuck in a "database out of sync" loop with a running daemon you can't stop, use these flags:
|
||||||
|
|
||||||
|
**1. Force metadata update (`--force` flag on import)**
|
||||||
|
|
||||||
|
When `bd import` reports "0 created, 0 updated" but staleness persists:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Force metadata refresh even when DB appears synced
|
||||||
|
bd import --force
|
||||||
|
|
||||||
|
# This updates internal metadata tracking without changing issues
|
||||||
|
# Fixes: stuck state caused by stale daemon cache
|
||||||
|
```
|
||||||
|
|
||||||
|
**Shows:** `Metadata updated (database already in sync with JSONL)`
|
||||||
|
|
||||||
|
**2. Skip staleness check (`--allow-stale` global flag)**
|
||||||
|
|
||||||
|
Emergency escape hatch to bypass staleness validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Allow operations on potentially stale data
|
||||||
|
bd --allow-stale ready
|
||||||
|
bd --allow-stale list --status open
|
||||||
|
|
||||||
|
# Shows warning:
|
||||||
|
# ⚠️ Staleness check skipped (--allow-stale), data may be out of sync
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ Caution:** Use sparingly - you may see incomplete or outdated data.
|
||||||
|
|
||||||
|
**3. Use sandbox mode (preferred)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Most reliable for sandboxed environments
|
||||||
|
bd --sandbox ready
|
||||||
|
bd --sandbox import -i .beads/beads.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Troubleshooting workflow
|
||||||
|
|
||||||
|
If stuck in a sandboxed environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Try sandbox mode (cleanest solution)
|
||||||
|
bd --sandbox ready
|
||||||
|
|
||||||
|
# Step 2: If you get staleness errors, force import
|
||||||
|
bd import --force -i .beads/beads.jsonl
|
||||||
|
|
||||||
|
# Step 3: If still blocked, use allow-stale (emergency only)
|
||||||
|
bd --allow-stale ready
|
||||||
|
|
||||||
|
# Step 4: When back outside sandbox, sync normally
|
||||||
|
bd sync
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Understanding the flags
|
||||||
|
|
||||||
|
| Flag | Purpose | When to use | Risk |
|
||||||
|
|------|---------|-------------|------|
|
||||||
|
| `--sandbox` | Disable daemon and auto-sync | Sandboxed environments (Codex, containers) | Low - safe for sandboxes |
|
||||||
|
| `--force` (import) | Force metadata update | Stuck "0 created, 0 updated" loop | Low - updates metadata only |
|
||||||
|
| `--allow-stale` | Skip staleness validation | Emergency access to database | **High** - may show stale data |
|
||||||
|
|
||||||
|
**Related:**
|
||||||
|
- See [DAEMON.md](DAEMON.md) for daemon troubleshooting
|
||||||
|
- See [Claude Code sandboxing documentation](https://www.anthropic.com/engineering/claude-code-sandboxing) for more about sandbox restrictions
|
||||||
|
- GitHub issue [#353](https://github.com/steveyegge/beads/issues/353) for background
|
||||||
|
|
||||||
## Platform-Specific Issues
|
## Platform-Specific Issues
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user