Skip to content

Enhance privacy and security by adding permission checks and improvin… #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,48 @@ paru -S opencode-ai-bin
go install github.com/opencode-ai/opencode@latest
```

## Important: Local Data Storage and Privacy

Opencode is designed to run locally on your machine. To provide features like conversation history and session context, Opencode stores certain data on your computer:

* **Conversation History:** All interactions, including your prompts, responses from the language model, and commands to/from tools, are saved.
* **File Contents:** When you use features that involve files (e.g., editing a file, referencing it in a prompt), the content of those files may be stored as part of your session data.

**Storage Details:**

* This data is stored in an **unencrypted SQLite database file** named `opencode.db`.
* The default location for this database is typically:
* `~/.opencode/opencode.db` (on macOS and Linux if `$XDG_CONFIG_HOME` is not set)
* `$XDG_CONFIG_HOME/opencode/opencode.db` (on Linux if `$XDG_CONFIG_HOME` is set)

**Security Considerations:**

* Because this data is stored locally and unencrypted, anyone with access to your user account on your computer could potentially access this information.
* We recommend following security best practices for your computer, such as:
* Using a strong account password.
* Enabling full-disk encryption (e.g., FileVault on macOS, BitLocker on Windows, or LUKS on Linux).
* Keeping your operating system and security software up to date.

**Data Deletion:**

* If you wish to remove all your conversation history and locally stored file content from Opencode, you can do so by manually deleting the `opencode.db` file from the directory specified above. Please ensure Opencode is not running when you do this.

#### Automatic Context Files

Please be aware that Opencode may automatically include content from certain files found in your project's root directory to provide better context to the language model. These files often include project-specific instructions or guidelines (e.g., `opencode.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, `.cursorrules`).

If such files exist in your project and contain sensitive information that you do not wish to share with the configured language model, please review their content or avoid using these specific file names for sensitive data in your project root. You can see the list of default files checked in the `defaultContextPaths` variable within the application's source code (`internal/config/config.go`).

### Tool Usage and Permissions

Opencode leverages powerful tools to interact with your system, such as executing shell commands (`bash` tool), fetching content from URLs (`fetch` tool), and modifying files (`edit`, `write`, `patch` tools). While these tools enable complex tasks, they also require careful handling:

* **Review Permission Prompts:** When a tool needs to perform a sensitive action (like writing a file or running a command), Opencode will ask for your permission. **It is crucial to carefully review the details of each permission request before approving it.**
* The language model generates the commands or parameters for these tools. Always verify that the action described in the prompt (e.g., the specific command to be run, the file to be changed, or the URL to be fetched) is exactly what you intend.
* Do not blindly approve requests, especially if they seem suspicious or unexpected.

* **Auto-Approve Sessions:** Opencode has a feature to "auto-approve" all permission requests within a specific session. While this can streamline workflows, it bypasses the safety net of individual prompts. Use this feature with extreme caution and only in situations where you fully trust the sequence of operations.

## Configuration

OpenCode looks for configuration in the following locations:
Expand Down
18 changes: 11 additions & 7 deletions internal/llm/agent/agent-tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import (
"github.com/opencode-ai/opencode/internal/llm/tools"
"github.com/opencode-ai/opencode/internal/lsp"
"github.com/opencode-ai/opencode/internal/message"
"github.com/opencode-ai/opencode/internal/permission"
"github.com/opencode-ai/opencode/internal/session"
)

type agentTool struct {
sessions session.Service
messages message.Service
lspClients map[string]*lsp.Client
sessions session.Service
messages message.Service
lspClients map[string]*lsp.Client
permissions permission.Service
}

const (
Expand Down Expand Up @@ -54,7 +56,7 @@ func (b *agentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolRes
return tools.ToolResponse{}, fmt.Errorf("session_id and message_id are required")
}

agent, err := NewAgent(config.AgentTask, b.sessions, b.messages, TaskAgentTools(b.lspClients))
agent, err := NewAgent(config.AgentTask, b.sessions, b.messages, TaskAgentTools(b.permissions, b.lspClients))
if err != nil {
return tools.ToolResponse{}, fmt.Errorf("error creating agent: %s", err)
}
Expand Down Expand Up @@ -100,10 +102,12 @@ func NewAgentTool(
Sessions session.Service,
Messages message.Service,
LspClients map[string]*lsp.Client,
Permissions permission.Service,
) tools.BaseTool {
return &agentTool{
sessions: Sessions,
messages: Messages,
lspClients: LspClients,
sessions: Sessions,
messages: Messages,
lspClients: LspClients,
permissions: Permissions,
}
}
16 changes: 8 additions & 8 deletions internal/llm/agent/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,23 @@ func CoderAgentTools(
tools.NewEditTool(lspClients, permissions, history),
tools.NewFetchTool(permissions),
tools.NewGlobTool(),
tools.NewGrepTool(),
tools.NewGrepTool(permissions),
tools.NewLsTool(),
tools.NewSourcegraphTool(),
tools.NewViewTool(lspClients),
tools.NewSourcegraphTool(permissions),
tools.NewViewTool(lspClients, permissions),
tools.NewPatchTool(lspClients, permissions, history),
tools.NewWriteTool(lspClients, permissions, history),
NewAgentTool(sessions, messages, lspClients),
NewAgentTool(sessions, messages, lspClients, permissions),
}, otherTools...,
)
}

func TaskAgentTools(lspClients map[string]*lsp.Client) []tools.BaseTool {
func TaskAgentTools(permissions permission.Service, lspClients map[string]*lsp.Client) []tools.BaseTool {
return []tools.BaseTool{
tools.NewGlobTool(),
tools.NewGrepTool(),
tools.NewGrepTool(permissions),
tools.NewLsTool(),
tools.NewSourcegraphTool(),
tools.NewViewTool(lspClients),
tools.NewSourcegraphTool(permissions),
tools.NewViewTool(lspClients, permissions),
}
}
32 changes: 29 additions & 3 deletions internal/llm/tools/grep.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/fileutil"
"github.com/opencode-ai/opencode/internal/llm/permission"
)

type GrepParams struct {
Expand All @@ -37,7 +38,9 @@ type GrepResponseMetadata struct {
Truncated bool `json:"truncated"`
}

type grepTool struct{}
type grepTool struct {
permissions permission.Service
}

const (
GrepToolName = "grep"
Expand Down Expand Up @@ -79,8 +82,10 @@ TIPS:
- Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`
)

func NewGrepTool() BaseTool {
return &grepTool{}
func NewGrepTool(permissions permission.Service) BaseTool {
return &grepTool{
permissions: permissions,
}
}

func (g *grepTool) Info() ToolInfo {
Expand Down Expand Up @@ -142,6 +147,27 @@ func (g *grepTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
searchPath = config.WorkingDirectory()
}

sessionID, messageID := GetContextValues(ctx)
if sessionID == "" || messageID == "" {
return ToolResponse{}, fmt.Errorf("session ID and message ID are required for this tool")
}

p := g.permissions.Request(
permission.CreatePermissionRequest{
SessionID: sessionID,
MessageID: messageID,
Path: searchPath,
ToolName: GrepToolName,
Action: "search_content",
Description: fmt.Sprintf("Search content in path '%s' for pattern: %s (include: %s)", searchPath, params.Pattern, params.Include),
Params: params,
},
)

if !p {
return ToolResponse{}, permission.ErrorPermissionDenied
}

matches, truncated, err := searchFiles(searchPattern, searchPath, params.Include, 100)
if err != nil {
return ToolResponse{}, fmt.Errorf("error searching files: %w", err)
Expand Down
32 changes: 30 additions & 2 deletions internal/llm/tools/sourcegraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"net/http"
"strings"
"time"

"github.com/sourcegraph/sourcegraph/internal/llm/permission"
"github.com/sourcegraph/sourcegraph/internal/llm/internal/config"
)

type SourcegraphParams struct {
Expand All @@ -24,7 +27,8 @@ type SourcegraphResponseMetadata struct {
}

type sourcegraphTool struct {
client *http.Client
client *http.Client
permissions permission.Service
}

const (
Expand Down Expand Up @@ -125,11 +129,12 @@ TIPS:
- Use type:file to find relevant files`
)

func NewSourcegraphTool() BaseTool {
func NewSourcegraphTool(permissions permission.Service) BaseTool {
return &sourcegraphTool{
client: &http.Client{
Timeout: 30 * time.Second,
},
permissions: permissions,
}
}

Expand Down Expand Up @@ -165,6 +170,29 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
return NewTextErrorResponse("Failed to parse sourcegraph parameters: " + err.Error()), nil
}

sessionID, messageID := GetContextValues(ctx)
if sessionID == "" || messageID == "" {
// This error handling might need adjustment if sourcegraph tool can operate without session/message IDs
// For now, assume they are required or find an alternative way to handle context for permissions.
return ToolResponse{}, fmt.Errorf("session ID and message ID are required for this tool")
}

p := t.permissions.Request(
permission.CreatePermissionRequest{
SessionID: sessionID,
MessageID: messageID,
Path: config.WorkingDirectory(), // Or a more relevant path if applicable
ToolName: SourcegraphToolName,
Action: "search",
Description: fmt.Sprintf("Search Sourcegraph for: %s", params.Query),
Params: params, // Sending the whole params struct
},
)

if !p {
return ToolResponse{}, permission.ErrorPermissionDenied
}

if params.Query == "" {
return NewTextErrorResponse("Query parameter is required"), nil
}
Expand Down
30 changes: 27 additions & 3 deletions internal/llm/tools/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/llm/permission"
"github.com/opencode-ai/opencode/internal/lsp"
)

Expand All @@ -21,7 +22,8 @@ type ViewParams struct {
}

type viewTool struct {
lspClients map[string]*lsp.Client
lspClients map[string]*lsp.Client
permissions permission.Service
}

type ViewResponseMetadata struct {
Expand Down Expand Up @@ -66,9 +68,10 @@ TIPS:
- When viewing large files, use the offset parameter to read specific sections`
)

func NewViewTool(lspClients map[string]*lsp.Client) BaseTool {
func NewViewTool(lspClients map[string]*lsp.Client, permissions permission.Service) BaseTool {
return &viewTool{
lspClients,
lspClients: lspClients,
permissions: permissions,
}
}

Expand Down Expand Up @@ -111,6 +114,27 @@ func (v *viewTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
filePath = filepath.Join(config.WorkingDirectory(), filePath)
}

sessionID, messageID := GetContextValues(ctx)
if sessionID == "" || messageID == "" {
return ToolResponse{}, fmt.Errorf("session ID and message ID are required for this tool")
}

p := v.permissions.Request(
permission.CreatePermissionRequest{
SessionID: sessionID,
MessageID: messageID,
Path: filePath,
ToolName: ViewToolName,
Action: "read",
Description: fmt.Sprintf("Read file content: %s", filePath),
Params: params,
},
)

if !p {
return ToolResponse{}, permission.ErrorPermissionDenied
}

// Check if file exists
fileInfo, err := os.Stat(filePath)
if err != nil {
Expand Down