Skip to content

Commit 5b07a6b

Browse files
committed
fix: resolve symlinks in allowed roots, handle Codex errors, allow Claude web tools
- Resolve symlinks via filepath.EvalSymlinks in allowed roots and path validation to fix macOS path mismatch (e.g. /tmp -> /private/tmp) - Include diagnostic info in "outside allowed roots" error messages - Handle Codex CLI "error" async method: emit turn.failed instead of hanging forever waiting for turn/completed - Allow Claude Code built-in read and web tools (WebSearch, WebFetch, Read, Glob, Grep) while keeping command/file ops through MCP
1 parent d895776 commit 5b07a6b

4 files changed

Lines changed: 47 additions & 3 deletions

File tree

internal/app/fs_tasks.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"errors"
7+
"fmt"
78
"io/fs"
89
"log"
910
"os"
@@ -234,8 +235,12 @@ func (s *Service) executeListDirectories(requestedPath string, limit int, cursor
234235
if err != nil {
235236
return daemonMachineDirectoryListResponse{}, errors.New("path must be absolute")
236237
}
238+
// Resolve symlinks for consistent comparison (macOS: /tmp → /private/tmp).
239+
if resolved, err := filepath.EvalSymlinks(targetPath); err == nil {
240+
targetPath = resolved
241+
}
237242
if !daemonPathWithinAnyRoot(targetPath, allowedRoots) {
238-
return daemonMachineDirectoryListResponse{}, errors.New("path is outside allowed roots")
243+
return daemonMachineDirectoryListResponse{}, fmt.Errorf("path %q is outside allowed roots %v", targetPath, allowedRoots)
239244
}
240245
info, err := os.Stat(targetPath)
241246
if err != nil {
@@ -516,9 +521,14 @@ func (s *Service) validateWorkspaceRoot(workspaceRoot string) (string, error) {
516521
if err != nil {
517522
return "", errors.New("workspace root is invalid")
518523
}
524+
// Resolve symlinks for consistent comparison with allowed roots
525+
// (macOS: /tmp → /private/tmp, etc.).
526+
if resolved, err := filepath.EvalSymlinks(absWorkspaceRoot); err == nil {
527+
absWorkspaceRoot = resolved
528+
}
519529
allowedRoots := s.currentAllowedRoots()
520530
if len(allowedRoots) > 0 && !daemonPathWithinAnyRoot(absWorkspaceRoot, allowedRoots) {
521-
return "", errors.New("workspace root is outside allowed roots")
531+
return "", fmt.Errorf("workspace root %q is outside allowed roots %v", absWorkspaceRoot, allowedRoots)
522532
}
523533
info, err := os.Stat(absWorkspaceRoot)
524534
if err != nil {

internal/app/machine_inventory.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ func normalizeRoots(inputs []string) []string {
4343
if err != nil {
4444
continue
4545
}
46+
// Resolve symlinks so that paths like /tmp (→ /private/tmp on
47+
// macOS) match correctly against the canonical allowed root.
48+
if resolved, err := filepath.EvalSymlinks(abs); err == nil {
49+
abs = resolved
50+
}
4651
info, err := os.Stat(abs)
4752
if err != nil || !info.IsDir() {
4853
continue

internal/app/runner_claude.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ func (r *claudeRunner) RunTurn(ctx context.Context, dispatch taskDispatch, provi
122122
"--output-format", "stream-json",
123123
"--verbose",
124124
"--mcp-config", mcpConfigPath,
125-
"--allowedTools", "mcp__pocketcode__run_command", "mcp__pocketcode__create_file",
125+
"--allowedTools",
126+
"mcp__pocketcode__run_command", "mcp__pocketcode__create_file",
127+
"WebSearch", "WebFetch",
128+
"Read", "Glob", "Grep",
126129
"--permission-mode", "default",
127130
"--system-prompt", systemPrompt,
128131
}

internal/app/runner_codex.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,32 @@ func (r *codexRunner) handleAsyncMessage(ctx context.Context, rpc *codexRPCClien
300300
},
301301
})
302302

303+
case msg.Method == "error":
304+
// Codex CLI sends async "error" notifications when something goes
305+
// wrong (e.g. API key invalid, model error, rate limit). Log the
306+
// full payload and emit turn.failed so the user sees the error
307+
// instead of the turn hanging forever.
308+
var errPayload struct {
309+
Message string `json:"message"`
310+
Code string `json:"code"`
311+
}
312+
_ = json.Unmarshal(msg.Params, &errPayload)
313+
errMsg := errPayload.Message
314+
if errMsg == "" {
315+
errMsg = string(msg.Params)
316+
}
317+
log.Printf("[CODEX] error notification: %s (code=%s, taskRun=%s)", errMsg, errPayload.Code, dispatch.TaskRunID)
318+
r.deltaBuf.Flush(ctx)
319+
r.closeAssistantStream(ctx, dispatch, state, "")
320+
return true, r.server.postEvent(ctx, daemonEvent{
321+
SessionID: dispatch.SessionID,
322+
TaskRunID: dispatch.TaskRunID,
323+
EventType: "turn.failed",
324+
Payload: map[string]any{
325+
"message": "Codex error: " + errMsg,
326+
},
327+
})
328+
303329
default:
304330
// Defensive: log unknown methods so future Codex CLI additions are
305331
// visible in daemon logs instead of being silently dropped.

0 commit comments

Comments
 (0)