diff --git a/pkg/tools/base.go b/pkg/tools/base.go index f3e83b35b..57209a1a6 100644 --- a/pkg/tools/base.go +++ b/pkg/tools/base.go @@ -2,6 +2,104 @@ package tools import "context" +// CapableTool is an optional interface for tools that require elevated +// capabilities: secrets, network endpoints, filesystem paths, or shell access. +// +// Tools that do not implement CapableTool receive zero-capability defaults: +// no secrets, no network, filesystem read-only to workspace, no shell. +// This is the correct default for safe, backward-compatible operation. +// +// The SecureBus reads Capabilities() before each execution, enforces the +// declared policy, injects secrets, and redacts output — all transparent to +// the tool's Execute method. +type CapableTool interface { + Tool + Capabilities() ToolCapabilities +} + +// ToolCapabilities declares what a tool needs at runtime. +// All fields are zero-value safe (nil slice / zero enum = deny). +type ToolCapabilities struct { + // Secrets lists named secrets the tool requires. The SecureBus resolves + // each from the SecretStore and injects it per the InjectAs spec. + Secrets []SecretRef + + // Network lists URL glob patterns the tool is permitted to contact. + // SSRF validation is still applied within permitted patterns. + Network []EndpointRule + + // Filesystem lists path rules the tool may access, relative to workspace root. + Filesystem []PathRule + + // Shell declares the shell access level for tools that execute subprocesses. + Shell ShellAccessLevel +} + +// SecretRef identifies a named secret and how to deliver it to the tool. +type SecretRef struct { + // Name is the logical secret identifier (e.g. "github_token"). + Name string + + // InjectAs describes how to deliver the secret: + // "env:VAR_NAME" — set environment variable + // "arg:key" — add to args map under key + // "header:Header-Name" — add as HTTP header (web tools) + InjectAs string + + // Required causes tool execution to fail if the secret is missing. + // When false, the tool executes with the secret omitted. + Required bool +} + +// EndpointRule permits access to a URL matching the given glob pattern. +// Patterns follow filepath.Match syntax applied to the full URL string. +// Example: "https://api.github.com/**" +type EndpointRule struct { + Pattern string +} + +// PathRule grants filesystem access to paths matching Pattern. +// Pattern is relative to the workspace root and uses filepath.Match syntax. +type PathRule struct { + // Pattern is the path glob, e.g. "data/**" or "config/*.toml". + Pattern string + + // Mode is one of "r" (read-only), "w" (write-only), "rw" (read-write). + Mode string +} + +// ShellAccessLevel controls subprocess execution for shell tools. +type ShellAccessLevel int + +const ( + // ShellNone disables all subprocess execution. + ShellNone ShellAccessLevel = iota + + // ShellDenylist allows subprocess execution but blocks known-dangerous patterns + // (e.g. rm -rf /, /dev/mem access, raw socket creation). This is the default + // for tools that declare Shell access. + ShellDenylist + + // ShellAllowlist restricts execution to an explicit permit list of command + // patterns. Any command not matching the list is rejected. + ShellAllowlist +) + +// ZeroCapabilities returns the default deny-all capabilities applied to any +// tool that does not implement CapableTool. +func ZeroCapabilities() ToolCapabilities { + return ToolCapabilities{Shell: ShellNone} +} + +// ExtractCapabilities returns a tool's declared capabilities, or ZeroCapabilities +// for tools that do not implement CapableTool. +func ExtractCapabilities(t Tool) ToolCapabilities { + if ct, ok := t.(CapableTool); ok { + return ct.Capabilities() + } + return ZeroCapabilities() +} + // Tool is the interface that all tools must implement. type Tool interface { Name() string diff --git a/pkg/tools/filesystem.go b/pkg/tools/filesystem.go index 55d1e313f..278bd3a41 100644 --- a/pkg/tools/filesystem.go +++ b/pkg/tools/filesystem.go @@ -148,6 +148,13 @@ func (t *ReadFileTool) Parameters() map[string]interface{} { } } +// Capabilities declares that ReadFileTool requires read-only filesystem access. +func (t *ReadFileTool) Capabilities() ToolCapabilities { + return ToolCapabilities{ + Filesystem: []PathRule{{Pattern: "**", Mode: "r"}}, + } +} + func (t *ReadFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { path, ok := args["path"].(string) if !ok { @@ -201,6 +208,13 @@ func (t *WriteFileTool) Parameters() map[string]interface{} { } } +// Capabilities declares that WriteFileTool requires read-write filesystem access. +func (t *WriteFileTool) Capabilities() ToolCapabilities { + return ToolCapabilities{ + Filesystem: []PathRule{{Pattern: "**", Mode: "rw"}}, + } +} + func (t *WriteFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { path, ok := args["path"].(string) if !ok { @@ -263,6 +277,13 @@ func (t *ListDirTool) Parameters() map[string]interface{} { } } +// Capabilities declares that ListDirTool requires read-only filesystem access. +func (t *ListDirTool) Capabilities() ToolCapabilities { + return ToolCapabilities{ + Filesystem: []PathRule{{Pattern: "**", Mode: "r"}}, + } +} + func (t *ListDirTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { path, ok := args["path"].(string) if !ok { diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index f8bfc74cd..580690552 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -165,6 +165,18 @@ func (t *ExecTool) Parameters() map[string]interface{} { } } +// Capabilities declares that ExecTool requires shell access with denylist +// enforcement. This satisfies the CapableTool interface so the SecureBus +// applies the correct policy without restricting existing behavior. +func (t *ExecTool) Capabilities() ToolCapabilities { + return ToolCapabilities{ + Shell: ShellDenylist, + Filesystem: []PathRule{ + {Pattern: "**", Mode: "rw"}, + }, + } +} + func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { command, ok := args["command"].(string) if !ok { diff --git a/pkg/tools/web.go b/pkg/tools/web.go index 6a6d40ecf..f4c6dfc72 100644 --- a/pkg/tools/web.go +++ b/pkg/tools/web.go @@ -314,6 +314,19 @@ func (t *WebSearchTool) Parameters() map[string]interface{} { } } +// Capabilities declares that WebSearchTool requires network access. +// The Brave Search API key is injected as a header by the SecureBus. +func (t *WebSearchTool) Capabilities() ToolCapabilities { + return ToolCapabilities{ + Secrets: []SecretRef{ + {Name: "brave_api_key", InjectAs: "header:X-Subscription-Token", Required: false}, + }, + Network: []EndpointRule{ + {Pattern: "https://api.search.brave.com/**"}, + }, + } +} + func (t *WebSearchTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { query, ok := args["query"].(string) if !ok { @@ -377,6 +390,17 @@ func (t *WebFetchTool) Parameters() map[string]interface{} { } } +// Capabilities declares that WebFetchTool requires broad network access. +// SSRF validation is applied within the SecureBus before any request is made. +func (t *WebFetchTool) Capabilities() ToolCapabilities { + return ToolCapabilities{ + Network: []EndpointRule{ + {Pattern: "https://**"}, + {Pattern: "http://**"}, + }, + } +} + func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { urlStr, ok := args["url"].(string) if !ok {