fix: wire permission handler to Copilot SDK (#161)

This commit is contained in:
João Salavisa
2026-02-04 10:17:49 +00:00
committed by GitHub
parent f21fa344c1
commit 9c8c02f4d8
2 changed files with 102 additions and 3 deletions
@@ -164,6 +164,9 @@ func (a *CopilotAdapter) Initialize(ctx context.Context) error {
// Set event handler before starting
a.client.SetEventHandler(a.handleEvent)
// Set permission handler before starting
a.client.SetPermissionHandler(a.handlePermissionRequest)
// Start the SDK client (connects to external server via TCP)
if err := a.client.Start(ctx); err != nil {
return fmt.Errorf("failed to start Copilot SDK client: %w", err)
@@ -795,6 +798,74 @@ func (a *CopilotAdapter) signalCompletion(success bool, errMsg string) {
}
}
// handlePermissionRequest handles permission requests from the Copilot SDK.
// This is called by the SDK when the agent needs permission for an action.
func (a *CopilotAdapter) handlePermissionRequest(
request copilot.PermissionRequest,
invocation copilot.PermissionInvocation,
) (copilot.PermissionRequestResult, error) {
a.mu.RLock()
handler := a.permissionHandler
sessionID := a.sessionID
a.mu.RUnlock()
// If no handler is set, auto-approve
if handler == nil {
a.logger.Debug("auto-approving permission (no handler)",
zap.String("kind", request.Kind),
zap.String("tool_call_id", request.ToolCallID))
return copilot.PermissionRequestResult{Kind: "approved"}, nil
}
// Build title from permission kind
title := fmt.Sprintf("Permission: %s", request.Kind)
if request.ToolCallID != "" {
title = fmt.Sprintf("Permission for tool %s: %s", request.ToolCallID, request.Kind)
}
// Convert Copilot permission request to kandev format
permReq := &PermissionRequest{
SessionID: sessionID,
ToolCallID: request.ToolCallID,
Title: title,
ActionType: request.Kind,
ActionDetails: request.Extra,
// Use tool_call_id as PendingID if available, otherwise generate one
PendingID: request.ToolCallID,
Options: []PermissionOption{
{OptionID: "allow", Name: "Allow", Kind: "allow_once"},
{OptionID: "deny", Name: "Deny", Kind: "reject_once"},
},
}
a.logger.Info("requesting permission from user",
zap.String("kind", request.Kind),
zap.String("tool_call_id", request.ToolCallID),
zap.String("session_id", sessionID))
// Call the permission handler (blocking - waits for user response)
ctx := context.Background()
resp, err := handler(ctx, permReq)
if err != nil {
a.logger.Warn("permission handler error", zap.Error(err))
return copilot.PermissionRequestResult{Kind: "denied-interactively-by-user"}, nil
}
if resp == nil || resp.Cancelled {
a.logger.Info("permission denied (cancelled)")
return copilot.PermissionRequestResult{Kind: "denied-interactively-by-user"}, nil
}
// Convert response
if resp.OptionID == "allow" {
a.logger.Info("permission approved")
return copilot.PermissionRequestResult{Kind: "approved"}, nil
}
a.logger.Info("permission denied")
return copilot.PermissionRequestResult{Kind: "denied-interactively-by-user"}, nil
}
// RequiresProcessKill returns true because the Copilot CLI server doesn't exit on stdin close.
// The CLI runs as an HTTP server that must be explicitly killed.
func (a *CopilotAdapter) RequiresProcessKill() bool {
+31 -3
View File
@@ -25,6 +25,11 @@ type (
SessionConfig = copilot.SessionConfig
MessageOptions = copilot.MessageOptions
Data = copilot.Data
// Permission types
PermissionHandler = copilot.PermissionHandler
PermissionRequest = copilot.PermissionRequest
PermissionInvocation = copilot.PermissionInvocation
PermissionRequestResult = copilot.PermissionRequestResult
)
// Re-export event type constants
@@ -62,6 +67,10 @@ type Client struct {
unsubscribe func()
handlerMu sync.RWMutex
// Permission handler
permissionHandler PermissionHandler
permissionMu sync.RWMutex
// State
sessionID string
mu sync.RWMutex
@@ -96,6 +105,13 @@ func (c *Client) SetEventHandler(handler func(SessionEvent)) {
c.eventHandler = handler
}
// SetPermissionHandler sets the handler for permission requests.
func (c *Client) SetPermissionHandler(handler PermissionHandler) {
c.permissionMu.Lock()
defer c.permissionMu.Unlock()
c.permissionHandler = handler
}
// Start initializes the Copilot SDK client.
// When CLIUrl is configured, the SDK connects to an external CLI server via TCP.
// Otherwise, the SDK spawns and manages the CLI process internally via stdio.
@@ -199,10 +215,16 @@ func (c *Client) CreateSession(ctx context.Context) (string, error) {
c.logger.Info("creating new session", zap.String("model", c.model))
// Get permission handler
c.permissionMu.RLock()
permHandler := c.permissionHandler
c.permissionMu.RUnlock()
// Create session with configuration
session, err := c.sdkClient.CreateSession(&copilot.SessionConfig{
Model: c.model,
Streaming: true,
Model: c.model,
Streaming: true,
OnPermissionRequest: permHandler,
})
if err != nil {
return "", fmt.Errorf("failed to create session: %w", err)
@@ -250,9 +272,15 @@ func (c *Client) ResumeSession(ctx context.Context, sessionID string) error {
c.logger.Info("resuming session", zap.String("session_id", sessionID))
// Get permission handler
c.permissionMu.RLock()
permHandler := c.permissionHandler
c.permissionMu.RUnlock()
// Use ResumeSessionWithOptions to enable streaming on resumed sessions
session, err := c.sdkClient.ResumeSessionWithOptions(sessionID, &copilot.ResumeSessionConfig{
Streaming: true,
Streaming: true,
OnPermissionRequest: permHandler,
})
if err != nil {
return fmt.Errorf("failed to resume session: %w", err)