Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cmd/gateway_tools_wiring.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ func wireExtraTools(
pa.AllowPaths(userAllowPaths...)
}
}
// exec working_dir validation shares the same allow-list as file tools so
// the agent can cd into a skill dir to run its scripts. Note: subagent
// ExecTool is constructed separately in buildSubagentToolsRegistry and
// does NOT inherit this — that's an intentional scope limit for now.
if execTool, ok := toolsReg.Get("exec"); ok {
if pa, ok := execTool.(tools.PathAllowable); ok {
pa.AllowPaths(skillsAllowPaths...)
pa.AllowPaths(userAllowPaths...)
}
}

// Memory tools are PG-backed; always available.
hasMemory = true
Expand Down
5 changes: 4 additions & 1 deletion internal/tools/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,10 @@ func resolvePathWithAllowed(path, workspace string, restrict bool, allowedPrefix
return real, nil
}
}
slog.Warn("read_file: access denied", "path", cleaned, "workspace", workspace, "allowedPrefixes", allowedPrefixes)
// Caller-neutral message: this function is shared by read_file, list_files,
// read_image, send_file, and exec (working_dir validation). The tool name
// is surfaced via the surrounding tool_call_update event.
slog.Warn("path access denied", "path", cleaned, "workspace", workspace, "allowedPrefixes", allowedPrefixes)
return "", err
}

Expand Down
14 changes: 13 additions & 1 deletion internal/tools/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type ExecTool struct {
pathDenyPatterns []*regexp.Regexp // always-on path-based denials (DenyPaths)
pathDenyRoots []string // raw deny roots for nested workspace exemptions
denyExemptions []string // substrings that exempt a command from deny
allowedPrefixes []string // extra path prefixes allowed as working_dir (AllowPaths)
restrict bool
sandboxMgr sandbox.Manager // nil = no sandbox, execute on host
approvalMgr *ExecApprovalManager // nil = no approval needed
Expand Down Expand Up @@ -127,6 +128,15 @@ func (t *ExecTool) AllowPathExemptions(prefixes ...string) {
t.denyExemptions = append(t.denyExemptions, prefixes...)
}

// AllowPaths adds path prefixes that exec is allowed to use as working_dir,
// even when restrict_to_workspace is true (e.g. skills directories).
// Implements the PathAllowable interface for uniform wiring with file tools.
// Distinct from AllowPathExemptions, which exempts command arguments from
// deny pattern matching.
func (t *ExecTool) AllowPaths(prefixes ...string) {
t.allowedPrefixes = append(t.allowedPrefixes, prefixes...)
}

// normalizeCommand applies NFKC Unicode normalization and strips zero-width
// characters before deny pattern matching, preventing Unicode-based bypasses.
func normalizeCommand(s string) string {
Expand Down Expand Up @@ -372,7 +382,9 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *Result {
// stricter write-allowed prefixes (team root excluded) to block
// cross-chat cwd even for "read-only" commands like cat, since we
// cannot prove the shell command will not write.
allowed := allowedWriteWithTeamWorkspace(ctx, nil)
// Base prefixes come from AllowPaths (skills-store, user-configured
// allow_paths) so the agent can cd into skill dirs to run scripts.
allowed := allowedWriteWithTeamWorkspace(ctx, t.allowedPrefixes)
resolved, err := resolvePathWithAllowed(wd, wsBase, true, allowed)
if err != nil {
return ErrorResult(err.Error())
Expand Down
4 changes: 3 additions & 1 deletion internal/tools/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ type ApprovalAware interface {
SetApprovalManager(*ExecApprovalManager, string)
}

// PathAllowable tools can allow extra path prefixes for read access.
// PathAllowable tools can allow extra path prefixes beyond the workspace.
// Each implementor interprets the prefixes against its own operation
// (read/write/edit/list target, or exec working_dir).
type PathAllowable interface {
AllowPaths(...string)
}
Expand Down
Loading