diff --git a/README.md b/README.md index c3e6a95a3e..7bc912c222 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/internal/llm/agent/agent-tool.go b/internal/llm/agent/agent-tool.go index 781720ded6..4e73405d2f 100644 --- a/internal/llm/agent/agent-tool.go +++ b/internal/llm/agent/agent-tool.go @@ -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 ( @@ -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) } @@ -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, } } diff --git a/internal/llm/agent/tools.go b/internal/llm/agent/tools.go index e6b0119aef..877cf8645f 100644 --- a/internal/llm/agent/tools.go +++ b/internal/llm/agent/tools.go @@ -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), } } diff --git a/internal/llm/tools/grep.go b/internal/llm/tools/grep.go index f20d61ef1e..95106ac809 100644 --- a/internal/llm/tools/grep.go +++ b/internal/llm/tools/grep.go @@ -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 { @@ -37,7 +38,9 @@ type GrepResponseMetadata struct { Truncated bool `json:"truncated"` } -type grepTool struct{} +type grepTool struct { + permissions permission.Service +} const ( GrepToolName = "grep" @@ -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 { @@ -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) diff --git a/internal/llm/tools/sourcegraph.go b/internal/llm/tools/sourcegraph.go index 0d38c975fb..7a688f6ef0 100644 --- a/internal/llm/tools/sourcegraph.go +++ b/internal/llm/tools/sourcegraph.go @@ -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 { @@ -24,7 +27,8 @@ type SourcegraphResponseMetadata struct { } type sourcegraphTool struct { - client *http.Client + client *http.Client + permissions permission.Service } const ( @@ -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, } } @@ -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 } diff --git a/internal/llm/tools/view.go b/internal/llm/tools/view.go index 6d800ce6ee..71582e4185 100644 --- a/internal/llm/tools/view.go +++ b/internal/llm/tools/view.go @@ -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" ) @@ -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 { @@ -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, } } @@ -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 {