mirror of
				https://github.com/humanlayer/humanlayer.git
				synced 2025-08-20 19:01:22 +03:00 
			
		
		
		
	Fix MCP server socket path for parallel dev environments
Pass HUMANLAYER_DAEMON_SOCKET environment variable to MCP servers launched by the daemon. This ensures MCP servers connect back to the correct daemon instance (dev vs nightly) instead of always using the default socket path. - Update session Manager to accept and store socket path - Add HUMANLAYER_DAEMON_SOCKET to MCP server environment variables - Update MCP server to read socket path from environment - Update all tests to pass socket path parameter
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -180,3 +180,5 @@ cython_debug/ | ||||
| .idea/ | ||||
|  | ||||
| hlyr/blah.txt | ||||
| hld/hld-dev | ||||
| hld/hld-nightly | ||||
|   | ||||
							
								
								
									
										172
									
								
								DEVELOPMENT.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								DEVELOPMENT.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| # Development Guide | ||||
|  | ||||
| This guide covers development workflows and tools for the HumanLayer repository. | ||||
|  | ||||
| ## Parallel Development Environments | ||||
|  | ||||
| The HumanLayer development setup supports running parallel daemon/WUI instances to prevent development work from disrupting active Claude sessions. You can run a stable "nightly" environment for regular work alongside a "dev" environment for testing changes. | ||||
|  | ||||
| ### Quick Start | ||||
|  | ||||
| ```bash | ||||
| # Start nightly (stable) environment | ||||
| make daemon-nightly | ||||
| make wui-nightly | ||||
|  | ||||
| # Start dev environment (in another terminal) | ||||
| make daemon-dev | ||||
| make wui-dev | ||||
|  | ||||
| # Launch Claude Code with specific daemon | ||||
| npx humanlayer launch "implement feature X" --daemon-socket ~/.humanlayer/daemon-dev.sock | ||||
| ``` | ||||
|  | ||||
| ### Environment Overview | ||||
|  | ||||
| | Component | Nightly (Stable) | Dev (Testing) | | ||||
| |-----------|------------------|---------------| | ||||
| | Daemon Binary | `hld/hld-nightly` | `hld/hld-dev` | | ||||
| | Socket Path | `~/.humanlayer/daemon.sock` | `~/.humanlayer/daemon-dev.sock` | | ||||
| | Database | `~/.humanlayer/daemon.db` | `~/.humanlayer/dev/daemon-TIMESTAMP.db` | | ||||
| | Log Files | `daemon-nightly-*.log` | `daemon-dev-*.log` | | ||||
| | WUI | Installed in `~/Applications` | Running in dev mode | | ||||
|  | ||||
| ### Available Commands | ||||
|  | ||||
| #### Nightly (Stable) Environment | ||||
| ```bash | ||||
| make daemon-nightly-build  # Build nightly daemon binary | ||||
| make daemon-nightly        # Build and run nightly daemon | ||||
| make wui-nightly-build     # Build nightly WUI | ||||
| make wui-nightly          # Build, install, and open nightly WUI | ||||
| ``` | ||||
|  | ||||
| #### Dev Environment | ||||
| ```bash | ||||
| make daemon-dev-build     # Build dev daemon binary | ||||
| make daemon-dev          # Build and run dev daemon with fresh DB copy | ||||
| make wui-dev            # Run WUI in dev mode connected to dev daemon | ||||
| make copy-db-to-dev     # Manually copy production DB to timestamped dev DB | ||||
| make cleanup-dev        # Clean up dev DBs and logs older than 10 days | ||||
| ``` | ||||
|  | ||||
| #### Status and Utilities | ||||
| ```bash | ||||
| make dev-status         # Show current dev environment status | ||||
| ``` | ||||
|  | ||||
| ### Claude Code Integration | ||||
|  | ||||
| **Note**: The `hlyr` package was updated on July 18th and needs to be reinstalled to use the latest flow: | ||||
| ```bash | ||||
| cd hlyr && npm i -g . | ||||
| ``` | ||||
|  | ||||
| The `npx humanlayer launch` command supports custom daemon sockets through multiple methods: | ||||
|  | ||||
| #### 1. Command-line Flag | ||||
| ```bash | ||||
| npx humanlayer launch "test my implementation" --daemon-socket ~/.humanlayer/daemon-dev.sock | ||||
| ``` | ||||
|  | ||||
| #### 2. Environment Variable | ||||
| ```bash | ||||
| HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock npx humanlayer launch "test feature" | ||||
| ``` | ||||
|  | ||||
| #### 3. Configuration File | ||||
| Add to your `humanlayer.json`: | ||||
| ```json | ||||
| { | ||||
|   "daemon_socket": "~/.humanlayer/daemon-dev.sock" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Typical Development Workflow | ||||
|  | ||||
| 1. **Start your nightly environment** (once per day): | ||||
|    ```bash | ||||
|    make daemon-nightly | ||||
|    make wui-nightly | ||||
|    ``` | ||||
|  | ||||
| 2. **Work on a feature branch**: | ||||
|    ```bash | ||||
|    git checkout -b feature/my-feature | ||||
|    # Make your changes | ||||
|    ``` | ||||
|  | ||||
| 3. **Test your changes** without disrupting nightly: | ||||
|    ```bash | ||||
|    # Terminal 1: Start dev daemon | ||||
|    make daemon-dev | ||||
|     | ||||
|    # Terminal 2: Test with Claude Code | ||||
|    npx humanlayer launch "test my feature" --daemon-socket ~/.humanlayer/daemon-dev.sock | ||||
|     | ||||
|    # Or use dev WUI | ||||
|    make wui-dev | ||||
|    ``` | ||||
|  | ||||
| 4. **Clean up old dev artifacts** (weekly): | ||||
|    ```bash | ||||
|    make cleanup-dev | ||||
|    ``` | ||||
|  | ||||
| ### Key Features | ||||
|  | ||||
| - **Isolated Databases**: Each dev daemon run gets a fresh copy of your production database | ||||
| - **Separate Sockets**: Dev and nightly daemons use different Unix sockets | ||||
| - **Version Identification**: Dev daemon shows version as "dev" in WUI | ||||
| - **Automatic Logging**: All daemon runs are logged with timestamps | ||||
| - **No Cross-Contamination**: Changes in dev environment don't affect your stable nightly setup | ||||
|  | ||||
| ### Environment Variables | ||||
|  | ||||
| Both daemon and WUI respect these environment variables: | ||||
|  | ||||
| - `HUMANLAYER_DAEMON_SOCKET`: Path to daemon socket (default: `~/.humanlayer/daemon.sock`) | ||||
| - `HUMANLAYER_DATABASE_PATH`: Path to SQLite database (daemon only) | ||||
| - `HUMANLAYER_DAEMON_VERSION_OVERRIDE`: Custom version string (daemon only) | ||||
|  | ||||
| ### Troubleshooting | ||||
|  | ||||
| **Both daemons won't start**: Check if old processes are running: | ||||
| ```bash | ||||
| ps aux | grep hld | grep -v grep | ||||
| ``` | ||||
|  | ||||
| **WUI can't connect**: Verify the socket path matches between WUI and daemon: | ||||
| ```bash | ||||
| # Check which sockets exist | ||||
| ls -la ~/.humanlayer/*.sock | ||||
| ``` | ||||
|  | ||||
| **Database issues**: Each dev daemon run creates a new timestamped database: | ||||
| ```bash | ||||
| # List all dev databases | ||||
| ls -la ~/.humanlayer/dev/ | ||||
| ``` | ||||
|  | ||||
| ## Other Development Commands | ||||
|  | ||||
| ### Building and Testing | ||||
|  | ||||
| ```bash | ||||
| make setup              # Resolve dependencies across monorepo | ||||
| make check-test        # Run all checks and tests | ||||
| make check             # Run linting and type checking | ||||
| make test              # Run all test suites | ||||
| ``` | ||||
|  | ||||
| ### Python Development | ||||
| ```bash | ||||
| make check-py          # Python linting and type checking | ||||
| make test-py           # Python tests | ||||
| ``` | ||||
|  | ||||
| ### TypeScript Development | ||||
| Check individual `package.json` files for specific commands, as package managers and test frameworks vary across projects. | ||||
|  | ||||
| ### Go Development | ||||
| Check `go.mod` for Go version requirements and look for `Makefile` in each Go project directory. | ||||
							
								
								
									
										93
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										93
									
								
								Makefile
									
									
									
									
									
								
							| @@ -476,3 +476,96 @@ daemon: | ||||
| 	@mkdir -p ~/.humanlayer/logs | ||||
| 	echo "$(logfileprefix) starting daemon in $(shell pwd)" > ~/.humanlayer/logs/daemon-$(logfileprefix).log | ||||
| 	cd hlyr && npm run build && ./dist/bin/hld 2>&1 | tee -a ~/.humanlayer/logs/daemon-$(logfileprefix).log | ||||
|  | ||||
| # Build nightly daemon binary | ||||
| .PHONY: daemon-nightly-build | ||||
| daemon-nightly-build: | ||||
| 	cd hld && go build -o hld-nightly ./cmd/hld | ||||
| 	@echo "Built nightly daemon binary: hld/hld-nightly" | ||||
|  | ||||
| # Run nightly daemon | ||||
| .PHONY: daemon-nightly | ||||
| daemon-nightly: daemon-nightly-build | ||||
| 	@mkdir -p ~/.humanlayer/logs | ||||
| 	echo "$$(date +%Y-%m-%d-%H-%M-%S) starting nightly daemon in $$(pwd)" > ~/.humanlayer/logs/daemon-nightly-$$(date +%Y-%m-%d-%H-%M-%S).log | ||||
| 	cd hld && ./hld-nightly 2>&1 | tee -a ~/.humanlayer/logs/daemon-nightly-$$(date +%Y-%m-%d-%H-%M-%S).log | ||||
|  | ||||
| # Build and install nightly WUI | ||||
| .PHONY: wui-nightly-build | ||||
| wui-nightly-build: | ||||
| 	cd humanlayer-wui && bun run tauri build | ||||
| 	@echo "Build complete. Installing to ~/Applications..." | ||||
| 	cp -r humanlayer-wui/src-tauri/target/release/bundle/macos/humanlayer-wui.app ~/Applications/ | ||||
| 	@echo "Installed WUI nightly to ~/Applications/humanlayer-wui.app" | ||||
|  | ||||
| # Open nightly WUI | ||||
| .PHONY: wui-nightly | ||||
| wui-nightly: wui-nightly-build | ||||
| 	@echo "Opening WUI nightly..." | ||||
| 	open ~/Applications/humanlayer-wui.app | ||||
|  | ||||
| # Copy production database to timestamped dev database | ||||
| .PHONY: copy-db-to-dev | ||||
| copy-db-to-dev: | ||||
| 	@mkdir -p ~/.humanlayer/dev | ||||
| 	$(eval TIMESTAMP := $(shell date +%Y-%m-%d-%H-%M-%S)) | ||||
| 	$(eval DEV_DB := ~/.humanlayer/dev/daemon-$(TIMESTAMP).db) | ||||
| 	@if [ -f ~/.humanlayer/daemon.db ]; then \ | ||||
| 		cp ~/.humanlayer/daemon.db $(DEV_DB); \ | ||||
| 		echo "Copied production database to: $(DEV_DB)" >&2; \ | ||||
| 		echo "$(DEV_DB)"; \ | ||||
| 	else \ | ||||
| 		echo "Error: Production database not found at ~/.humanlayer/daemon.db" >&2; \ | ||||
| 		exit 1; \ | ||||
| 	fi | ||||
|  | ||||
| # Clean up dev databases and logs older than 10 days | ||||
| .PHONY: cleanup-dev | ||||
| cleanup-dev: | ||||
| 	@echo "Cleaning up dev artifacts older than 10 days..." | ||||
| 	@# Clean old dev databases | ||||
| 	@if [ -d ~/.humanlayer/dev ]; then \ | ||||
| 		find ~/.humanlayer/dev -name "daemon-*.db" -type f -mtime +10 -delete -print | sed 's/^/Deleted database: /'; \ | ||||
| 	fi | ||||
| 	@# Clean old dev logs | ||||
| 	@if [ -d ~/.humanlayer/logs ]; then \ | ||||
| 		find ~/.humanlayer/logs -name "*-dev-*.log" -type f -mtime +10 -delete -print | sed 's/^/Deleted log: /'; \ | ||||
| 	fi | ||||
| 	@echo "Cleanup complete." | ||||
|  | ||||
| # Build dev daemon binary | ||||
| .PHONY: daemon-dev-build | ||||
| daemon-dev-build: | ||||
| 	cd hld && go build -o hld-dev ./cmd/hld | ||||
| 	@echo "Built dev daemon binary: hld/hld-dev" | ||||
|  | ||||
| # Run dev daemon with fresh database copy | ||||
| .PHONY: daemon-dev | ||||
| daemon-dev: daemon-dev-build | ||||
| 	@mkdir -p ~/.humanlayer/logs | ||||
| 	$(eval TIMESTAMP := $(shell date +%Y-%m-%d-%H-%M-%S)) | ||||
| 	$(eval DEV_DB := $(shell make -s copy-db-to-dev)) | ||||
| 	@echo "Starting dev daemon with database: $(DEV_DB)" | ||||
| 	echo "$(TIMESTAMP) starting dev daemon in $$(pwd)" > ~/.humanlayer/logs/daemon-dev-$(TIMESTAMP).log | ||||
| 	cd hld && HUMANLAYER_DATABASE_PATH=$(DEV_DB) HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock HUMANLAYER_DAEMON_VERSION_OVERRIDE=dev ./hld-dev 2>&1 | tee -a ~/.humanlayer/logs/daemon-dev-$(TIMESTAMP).log | ||||
|  | ||||
| # Run dev WUI with custom socket | ||||
| .PHONY: wui-dev | ||||
| wui-dev: | ||||
| 	@mkdir -p ~/.humanlayer/logs | ||||
| 	$(eval TIMESTAMP := $(shell date +%Y-%m-%d-%H-%M-%S)) | ||||
| 	echo "$(TIMESTAMP) starting dev wui in $$(pwd)" > ~/.humanlayer/logs/wui-dev-$(TIMESTAMP).log | ||||
| 	cd humanlayer-wui && HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock bun run tauri dev 2>&1 | tee -a ~/.humanlayer/logs/wui-dev-$(TIMESTAMP).log | ||||
|  | ||||
| # Show current dev environment setup | ||||
| .PHONY: dev-status | ||||
| dev-status: | ||||
| 	@echo "=== Development Environment Status ===" | ||||
| 	@echo "Dev Socket: ~/.humanlayer/daemon-dev.sock" | ||||
| 	@echo "Nightly Socket: ~/.humanlayer/daemon.sock" | ||||
| 	@echo "" | ||||
| 	@echo "Dev Databases:" | ||||
| 	@ls -la ~/.humanlayer/dev/*.db 2>/dev/null || echo "  No dev databases found" | ||||
| 	@echo "" | ||||
| 	@echo "Active daemons:" | ||||
| 	@ps aux | grep -E "hld(-dev|-nightly)?$$" | grep -v grep || echo "  No daemons running" | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path/filepath" | ||||
| @@ -96,6 +97,10 @@ func (c *Client) buildArgs(config SessionConfig) ([]string, error) { | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to marshal MCP config: %w", err) | ||||
| 		} | ||||
| 		 | ||||
| 		// Log MCP config for debugging | ||||
| 		log.Printf("MCP config JSON: %s", string(mcpJSON)) | ||||
| 		 | ||||
| 		// Create a temp file for MCP config | ||||
| 		tmpFile, err := os.CreateTemp("", "mcp-config-*.json") | ||||
| 		if err != nil { | ||||
| @@ -107,6 +112,8 @@ func (c *Client) buildArgs(config SessionConfig) ([]string, error) { | ||||
| 			return nil, fmt.Errorf("failed to write MCP config: %w", err) | ||||
| 		} | ||||
| 		_ = tmpFile.Close() | ||||
| 		 | ||||
| 		log.Printf("MCP config written to: %s", tmpFile.Name()) | ||||
|  | ||||
| 		args = append(args, "--mcp-config", tmpFile.Name()) | ||||
| 		// Note: temp file will be cleaned up when process exits | ||||
| @@ -153,6 +160,7 @@ func (c *Client) Launch(config SessionConfig) (*Session, error) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	log.Printf("Executing Claude command: %s %v", c.claudePath, args) | ||||
| 	cmd := exec.Command(c.claudePath, args...) | ||||
|  | ||||
| 	// Set working directory if specified | ||||
|   | ||||
| @@ -22,6 +22,9 @@ type Config struct { | ||||
|  | ||||
| 	// Logging configuration | ||||
| 	LogLevel string `mapstructure:"log_level"` | ||||
|  | ||||
| 	// Version override for display purposes (e.g., "dev" for development instances) | ||||
| 	VersionOverride string `mapstructure:"version_override"` | ||||
| } | ||||
|  | ||||
| // Load loads configuration with priority: flags > env vars > config file > defaults | ||||
| @@ -47,6 +50,7 @@ func Load() (*Config, error) { | ||||
| 	_ = v.BindEnv("api_key", "HUMANLAYER_API_KEY") | ||||
| 	_ = v.BindEnv("api_base_url", "HUMANLAYER_API_BASE_URL", "HUMANLAYER_API_BASE") | ||||
| 	_ = v.BindEnv("log_level", "HUMANLAYER_LOG_LEVEL") | ||||
| 	_ = v.BindEnv("version_override", "HUMANLAYER_DAEMON_VERSION_OVERRIDE") | ||||
|  | ||||
| 	// Set defaults | ||||
| 	v.SetDefault("socket_path", "~/.humanlayer/daemon.sock") | ||||
|   | ||||
| @@ -89,7 +89,7 @@ func New() (*Daemon, error) { | ||||
| 	} | ||||
|  | ||||
| 	// Create session manager with store and config | ||||
| 	sessionManager, err := session.NewManager(eventBus, conversationStore) | ||||
| 	sessionManager, err := session.NewManager(eventBus, conversationStore, cfg.SocketPath) | ||||
| 	if err != nil { | ||||
| 		_ = conversationStore.Close() | ||||
| 		return nil, fmt.Errorf("failed to create session manager: %w", err) | ||||
| @@ -147,7 +147,11 @@ func (d *Daemon) Run(ctx context.Context) error { | ||||
| 	}() | ||||
|  | ||||
| 	// Create and start RPC server | ||||
| 	d.rpcServer = rpc.NewServer() | ||||
| 	if d.config.VersionOverride != "" { | ||||
| 		d.rpcServer = rpc.NewServerWithVersionOverride(d.config.VersionOverride) | ||||
| 	} else { | ||||
| 		d.rpcServer = rpc.NewServer() | ||||
| 	} | ||||
|  | ||||
| 	// Mark orphaned sessions as failed (from previous daemon run) | ||||
| 	if err := d.markOrphanedSessionsAsFailed(ctx); err != nil { | ||||
|   | ||||
| @@ -37,7 +37,7 @@ func TestDaemonApprovalIntegration(t *testing.T) { | ||||
| 	approvalManager := approval.NewManager(testStore, eventBus) | ||||
|  | ||||
| 	// Create session manager | ||||
| 	sessionManager, err := session.NewManager(eventBus, testStore) | ||||
| 	sessionManager, err := session.NewManager(eventBus, testStore, "") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("failed to create session manager: %v", err) | ||||
| 	} | ||||
|   | ||||
| @@ -30,7 +30,7 @@ func TestIntegrationContinueSession(t *testing.T) { | ||||
| 	} | ||||
| 	defer func() { _ = sqliteStore.Close() }() | ||||
|  | ||||
| 	sessionManager, err := session.NewManager(eventBus, sqliteStore) | ||||
| 	sessionManager, err := session.NewManager(eventBus, sqliteStore, "") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Failed to create session manager: %v", err) | ||||
| 	} | ||||
|   | ||||
| @@ -30,7 +30,7 @@ func TestIntegrationResumeDuringRunning(t *testing.T) { | ||||
| 	} | ||||
| 	defer func() { _ = sqliteStore.Close() }() | ||||
|  | ||||
| 	sessionManager, err := session.NewManager(eventBus, sqliteStore) | ||||
| 	sessionManager, err := session.NewManager(eventBus, sqliteStore, "") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Failed to create session manager: %v", err) | ||||
| 	} | ||||
|   | ||||
| @@ -41,7 +41,7 @@ func TestDaemonSessionStateIntegration(t *testing.T) { | ||||
| 	approvalManager := approval.NewManager(testStore, eventBus) | ||||
|  | ||||
| 	// Create session manager | ||||
| 	sessionManager, err := session.NewManager(eventBus, testStore) | ||||
| 	sessionManager, err := session.NewManager(eventBus, testStore, "") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("failed to create session manager: %v", err) | ||||
| 	} | ||||
|   | ||||
| @@ -18,6 +18,7 @@ type Server struct { | ||||
| 	connHandlers    map[string]ConnHandlerFunc | ||||
| 	subscriptionMgr *SubscriptionHandlers | ||||
| 	mu              sync.RWMutex | ||||
| 	versionOverride string | ||||
| } | ||||
|  | ||||
| // HandlerFunc is a function that handles an RPC method | ||||
| @@ -39,6 +40,20 @@ func NewServer() *Server { | ||||
| 	return s | ||||
| } | ||||
|  | ||||
| // NewServerWithVersionOverride creates a new RPC server with a custom version string | ||||
| func NewServerWithVersionOverride(versionOverride string) *Server { | ||||
| 	s := &Server{ | ||||
| 		handlers:        make(map[string]HandlerFunc), | ||||
| 		connHandlers:    make(map[string]ConnHandlerFunc), | ||||
| 		versionOverride: versionOverride, | ||||
| 	} | ||||
|  | ||||
| 	// Register built-in handlers | ||||
| 	s.registerBuiltinHandlers() | ||||
|  | ||||
| 	return s | ||||
| } | ||||
|  | ||||
| // registerBuiltinHandlers registers the default RPC methods | ||||
| func (s *Server) registerBuiltinHandlers() { | ||||
| 	s.Register("health", s.handleHealthCheck) | ||||
| @@ -233,8 +248,12 @@ func (s *Server) sendResponse(conn net.Conn, resp *Response) error { | ||||
|  | ||||
| // handleHealthCheck handles the health check RPC method | ||||
| func (s *Server) handleHealthCheck(ctx context.Context, params json.RawMessage) (interface{}, error) { | ||||
| 	version := Version | ||||
| 	if s.versionOverride != "" { | ||||
| 		version = s.versionOverride | ||||
| 	} | ||||
| 	return &HealthCheckResponse{ | ||||
| 		Status:  "ok", | ||||
| 		Version: Version, | ||||
| 		Version: version, | ||||
| 	}, nil | ||||
| } | ||||
|   | ||||
| @@ -22,7 +22,7 @@ func TestContinueSessionInheritance(t *testing.T) { | ||||
| 	} | ||||
| 	defer func() { _ = sqliteStore.Close() }() | ||||
|  | ||||
| 	manager, err := NewManager(eventBus, sqliteStore) | ||||
| 	manager, err := NewManager(eventBus, sqliteStore, "") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Failed to create manager: %v", err) | ||||
| 	} | ||||
|   | ||||
| @@ -26,13 +26,14 @@ type Manager struct { | ||||
| 	store              store.ConversationStore | ||||
| 	approvalReconciler ApprovalReconciler | ||||
| 	pendingQueries     sync.Map // map[sessionID]query - stores queries waiting for Claude session ID | ||||
| 	socketPath         string   // Daemon socket path for MCP servers | ||||
| } | ||||
|  | ||||
| // Compile-time check that Manager implements SessionManager | ||||
| var _ SessionManager = (*Manager)(nil) | ||||
|  | ||||
| // NewManager creates a new session manager with required store | ||||
| func NewManager(eventBus bus.EventBus, store store.ConversationStore) (*Manager, error) { | ||||
| func NewManager(eventBus bus.EventBus, store store.ConversationStore, socketPath string) (*Manager, error) { | ||||
| 	if store == nil { | ||||
| 		return nil, fmt.Errorf("store is required") | ||||
| 	} | ||||
| @@ -47,6 +48,7 @@ func NewManager(eventBus bus.EventBus, store store.ConversationStore) (*Manager, | ||||
| 		client:          client, | ||||
| 		eventBus:        eventBus, | ||||
| 		store:           store, | ||||
| 		socketPath:      socketPath, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| @@ -63,7 +65,7 @@ func (m *Manager) LaunchSession(ctx context.Context, config claudecode.SessionCo | ||||
| 	sessionID := uuid.New().String() | ||||
| 	runID := uuid.New().String() | ||||
|  | ||||
| 	// Add HUMANLAYER_RUN_ID to MCP server environment | ||||
| 	// Add HUMANLAYER_RUN_ID and HUMANLAYER_DAEMON_SOCKET to MCP server environment | ||||
| 	if config.MCPConfig != nil { | ||||
| 		slog.Debug("configuring MCP servers", "count", len(config.MCPConfig.MCPServers)) | ||||
| 		for name, server := range config.MCPConfig.MCPServers { | ||||
| @@ -71,12 +73,17 @@ func (m *Manager) LaunchSession(ctx context.Context, config claudecode.SessionCo | ||||
| 				server.Env = make(map[string]string) | ||||
| 			} | ||||
| 			server.Env["HUMANLAYER_RUN_ID"] = runID | ||||
| 			// Add daemon socket path so MCP servers connect to the correct daemon | ||||
| 			if m.socketPath != "" { | ||||
| 				server.Env["HUMANLAYER_DAEMON_SOCKET"] = m.socketPath | ||||
| 			} | ||||
| 			config.MCPConfig.MCPServers[name] = server | ||||
| 			slog.Debug("configured MCP server", | ||||
| 				"name", name, | ||||
| 				"command", server.Command, | ||||
| 				"args", server.Args, | ||||
| 				"run_id", runID) | ||||
| 				"run_id", runID, | ||||
| 				"socket_path", m.socketPath) | ||||
| 		} | ||||
| 	} else { | ||||
| 		slog.Debug("no MCP config provided") | ||||
| @@ -115,9 +122,29 @@ func (m *Manager) LaunchSession(ctx context.Context, config claudecode.SessionCo | ||||
|  | ||||
| 	// No longer storing full session in memory | ||||
|  | ||||
| 	// Log final configuration before launching | ||||
| 	var mcpServersDetail string | ||||
| 	if config.MCPConfig != nil { | ||||
| 		for name, server := range config.MCPConfig.MCPServers { | ||||
| 			mcpServersDetail += fmt.Sprintf("[%s: cmd=%s args=%v env=%v] ", name, server.Command, server.Args, server.Env) | ||||
| 		} | ||||
| 	} | ||||
| 	slog.Info("launching Claude session with configuration", | ||||
| 		"session_id", sessionID, | ||||
| 		"run_id", runID, | ||||
| 		"query", config.Query, | ||||
| 		"working_dir", config.WorkingDir, | ||||
| 		"permission_prompt_tool", config.PermissionPromptTool, | ||||
| 		"mcp_servers", len(config.MCPConfig.MCPServers), | ||||
| 		"mcp_servers_detail", mcpServersDetail) | ||||
|  | ||||
| 	// Launch Claude session | ||||
| 	claudeSession, err := m.client.Launch(config) | ||||
| 	if err != nil { | ||||
| 		slog.Error("failed to launch Claude session", | ||||
| 			"session_id", sessionID, | ||||
| 			"error", err, | ||||
| 			"config", fmt.Sprintf("%+v", config)) | ||||
| 		m.updateSessionStatus(ctx, sessionID, StatusFailed, err.Error()) | ||||
| 		return nil, fmt.Errorf("failed to launch Claude session: %w", err) | ||||
| 	} | ||||
| @@ -1065,13 +1092,17 @@ func (m *Manager) ContinueSession(ctx context.Context, req ContinueSessionConfig | ||||
| 		return nil, fmt.Errorf("failed to store session in database: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Add run_id to MCP server environments | ||||
| 	// Add run_id and daemon socket to MCP server environments | ||||
| 	if config.MCPConfig != nil { | ||||
| 		for name, server := range config.MCPConfig.MCPServers { | ||||
| 			if server.Env == nil { | ||||
| 				server.Env = make(map[string]string) | ||||
| 			} | ||||
| 			server.Env["HUMANLAYER_RUN_ID"] = runID | ||||
| 			// Add daemon socket path so MCP servers connect to the correct daemon | ||||
| 			if m.socketPath != "" { | ||||
| 				server.Env["HUMANLAYER_DAEMON_SOCKET"] = m.socketPath | ||||
| 			} | ||||
| 			config.MCPConfig.MCPServers[name] = server | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -19,7 +19,7 @@ func TestNewManager(t *testing.T) { | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
|  | ||||
| 	var eventBus bus.EventBus = nil // no bus for this test | ||||
| 	manager, err := NewManager(eventBus, mockStore) | ||||
| 	manager, err := NewManager(eventBus, mockStore, "") | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Failed to create manager: %v", err) | ||||
| @@ -34,7 +34,7 @@ func TestNewManager(t *testing.T) { | ||||
|  | ||||
| func TestNewManager_RequiresStore(t *testing.T) { | ||||
| 	var eventBus bus.EventBus = nil | ||||
| 	_, err := NewManager(eventBus, nil) | ||||
| 	_, err := NewManager(eventBus, nil, "") | ||||
|  | ||||
| 	if err == nil { | ||||
| 		t.Fatal("Expected error when store is nil") | ||||
| @@ -50,7 +50,7 @@ func TestListSessions(t *testing.T) { | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
| 	manager, _ := NewManager(nil, mockStore) | ||||
| 	manager, _ := NewManager(nil, mockStore, "") | ||||
|  | ||||
| 	// Test empty list | ||||
| 	mockStore.EXPECT().ListSessions(gomock.Any()).Return([]*store.Session{}, nil) | ||||
| @@ -83,7 +83,7 @@ func TestGetSessionInfo(t *testing.T) { | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
| 	manager, _ := NewManager(nil, mockStore) | ||||
| 	manager, _ := NewManager(nil, mockStore, "") | ||||
|  | ||||
| 	// Test not found | ||||
| 	mockStore.EXPECT().GetSession(gomock.Any(), "not-found").Return(nil, fmt.Errorf("not found")) | ||||
| @@ -121,7 +121,7 @@ func TestContinueSession_ValidatesParentExists(t *testing.T) { | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
| 	manager, _ := NewManager(nil, mockStore) | ||||
| 	manager, _ := NewManager(nil, mockStore, "") | ||||
|  | ||||
| 	// Test parent not found | ||||
| 	mockStore.EXPECT().GetSession(gomock.Any(), "not-found").Return(nil, fmt.Errorf("session not found")) | ||||
| @@ -144,7 +144,7 @@ func TestContinueSession_ValidatesParentStatus(t *testing.T) { | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
| 	manager, _ := NewManager(nil, mockStore) | ||||
| 	manager, _ := NewManager(nil, mockStore, "") | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name          string | ||||
| @@ -200,7 +200,7 @@ func TestContinueSession_ValidatesClaudeSessionID(t *testing.T) { | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
| 	manager, _ := NewManager(nil, mockStore) | ||||
| 	manager, _ := NewManager(nil, mockStore, "") | ||||
|  | ||||
| 	// Parent without claude_session_id | ||||
| 	parentSession := &store.Session{ | ||||
| @@ -232,7 +232,7 @@ func TestContinueSession_ValidatesWorkingDirectory(t *testing.T) { | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
| 	manager, _ := NewManager(nil, mockStore) | ||||
| 	manager, _ := NewManager(nil, mockStore, "") | ||||
|  | ||||
| 	// Parent without working directory | ||||
| 	parentSession := &store.Session{ | ||||
| @@ -264,7 +264,7 @@ func TestContinueSession_CreatesNewSessionWithParentReference(t *testing.T) { | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
| 	manager, _ := NewManager(nil, mockStore) | ||||
| 	manager, _ := NewManager(nil, mockStore, "") | ||||
|  | ||||
| 	// Mock parent session | ||||
| 	parentSession := &store.Session{ | ||||
| @@ -340,7 +340,7 @@ func TestContinueSession_HandlesOptionalOverrides(t *testing.T) { | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
| 	manager, _ := NewManager(nil, mockStore) | ||||
| 	manager, _ := NewManager(nil, mockStore, "") | ||||
|  | ||||
| 	// Mock parent session | ||||
| 	parentSession := &store.Session{ | ||||
| @@ -439,7 +439,7 @@ func TestInterruptSession(t *testing.T) { | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
| 	manager, _ := NewManager(nil, mockStore) | ||||
| 	manager, _ := NewManager(nil, mockStore, "") | ||||
|  | ||||
| 	// Test interrupting non-existent session | ||||
| 	err := manager.InterruptSession(context.Background(), "not-found") | ||||
| @@ -468,7 +468,7 @@ func TestContinueSession_InterruptsRunningSession(t *testing.T) { | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mockStore := store.NewMockConversationStore(ctrl) | ||||
| 	manager, _ := NewManager(nil, mockStore) | ||||
| 	manager, _ := NewManager(nil, mockStore, "") | ||||
|  | ||||
| 	t.Run("running session without claude_session_id", func(t *testing.T) { | ||||
| 		// Create a running parent session without claude_session_id (orphaned state) | ||||
|   | ||||
| @@ -35,13 +35,18 @@ export const launchCommand = async (query: string, options: LaunchOptions = {}) | ||||
|  | ||||
|     try { | ||||
|       // Build MCP config (approvals enabled by default unless explicitly disabled) | ||||
|       // For development, use the local built version directly | ||||
|       const scriptPath = new URL(import.meta.url).pathname | ||||
|       const projectRoot = scriptPath.split('/hlyr/')[0] + '/hlyr' | ||||
|       const localHlyrPath = `${projectRoot}/dist/index.js` | ||||
|        | ||||
|       const mcpConfig = | ||||
|         options.approvals !== false | ||||
|           ? { | ||||
|               mcpServers: { | ||||
|                 approvals: { | ||||
|                   command: 'npx', | ||||
|                   args: ['humanlayer', 'mcp', 'claude_approvals'], | ||||
|                   command: 'node', | ||||
|                   args: [localHlyrPath, 'mcp', 'claude_approvals'], | ||||
|                 }, | ||||
|               }, | ||||
|             } | ||||
|   | ||||
| @@ -75,7 +75,7 @@ program | ||||
|   .description('HumanLayer, but on your command-line.') | ||||
|   .version(packageJson.version) | ||||
|  | ||||
| const UNPROTECTED_COMMANDS = ['config', 'login', 'thoughts', 'join-waitlist'] | ||||
| const UNPROTECTED_COMMANDS = ['config', 'login', 'thoughts', 'join-waitlist', 'launch', 'mcp'] | ||||
|  | ||||
| program.hook('preAction', async (thisCmd, actionCmd) => { | ||||
|   // Get the full command path by traversing up the command hierarchy | ||||
|   | ||||
| @@ -116,8 +116,11 @@ export async function startClaudeApprovalsMCPServer() { | ||||
|     }, | ||||
|   ) | ||||
|  | ||||
|   // Create daemon client | ||||
|   const daemonClient = new DaemonClient() | ||||
|   // Create daemon client with socket path from environment or config | ||||
|   // The daemon sets HUMANLAYER_DAEMON_SOCKET for MCP servers it launches | ||||
|   const socketPath = process.env.HUMANLAYER_DAEMON_SOCKET || config.daemon_socket | ||||
|   logger.info('Creating daemon client', { socketPath }) | ||||
|   const daemonClient = new DaemonClient(socketPath) | ||||
|  | ||||
|   server.setRequestHandler(ListToolsRequestSchema, async () => { | ||||
|     return { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| use crate::daemon_client::error::{Error, Result}; | ||||
| use std::env; | ||||
| use std::path::PathBuf; | ||||
| use std::time::Duration; | ||||
| use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; | ||||
| @@ -103,6 +104,12 @@ impl Connection { | ||||
|  | ||||
|     /// Get the default socket path | ||||
|     fn default_socket_path() -> PathBuf { | ||||
|         // Check environment variable first, matching daemon's behavior | ||||
|         if let Ok(socket_path) = env::var("HUMANLAYER_DAEMON_SOCKET") { | ||||
|             return PathBuf::from(socket_path); | ||||
|         } | ||||
|  | ||||
|         // Fall back to default | ||||
|         let home = dirs::home_dir().expect("Could not find home directory"); | ||||
|         home.join(DEFAULT_SOCKET_PATH) | ||||
|     } | ||||
|   | ||||
| @@ -120,11 +120,13 @@ export const useSessionLauncher = create<LauncherState>((set, get) => ({ | ||||
|       set({ isLaunching: true, error: undefined }) | ||||
|  | ||||
|       // Build MCP config (approvals enabled by default) | ||||
|       // Use local development version of hlyr instead of npx to avoid version conflicts | ||||
|       const hlyrPath = '/Users/dex/wt/humanlayer/eng-1666/hlyr/dist/index.js' | ||||
|       const mcpConfig = { | ||||
|         mcpServers: { | ||||
|           approvals: { | ||||
|             command: 'npx', | ||||
|             args: ['humanlayer', 'mcp', 'claude_approvals'], | ||||
|             command: 'node', | ||||
|             args: [hlyrPath, 'mcp', 'claude_approvals'], | ||||
|           }, | ||||
|         }, | ||||
|       } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 dexhorthy
					dexhorthy