From 70614c8e545ab53aeef65153b3524d86b26943e7 Mon Sep 17 00:00:00 2001 From: ZanzyTHEbar Date: Thu, 19 Feb 2026 12:12:29 +0000 Subject: [PATCH] feat(tools): add CapableTool interface and Capabilities() declarations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce CapableTool extending the base Tool interface. Each tool may optionally implement Capabilities() → ToolCapabilities to declare: - Secrets it needs injected (name, env var, header or arg target) - Network endpoints it may reach (SSRF allow-list) - Filesystem path rules (read/write/deny, path prefix) - Shell access level (none, denylist, allowlist, unrestricted) Implementations: - ExecTool: ShellDenylist + ReadWrite filesystem - ReadFileTool: ReadOnly filesystem - WriteFileTool: ReadWrite filesystem - ListDirTool: ReadOnly filesystem - WebSearchTool: brave_api_key secret injection (header), Brave API endpoint - WebFetchTool: broad HTTP/HTTPS network access ZeroCapabilities() and ExtractCapabilities() helpers provided so callers can safely handle tools that do not implement CapableTool. This capability metadata is consumed by the SecureBus (ITR) to enforce policy, inject secrets, and scan output for leaks. --- pkg/tools/base.go | 98 +++++++++++++++++++++++++++++++++++++++++ pkg/tools/filesystem.go | 21 +++++++++ pkg/tools/shell.go | 12 +++++ pkg/tools/web.go | 24 ++++++++++ 4 files changed, 155 insertions(+) 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 {