tools: one file per builtin tool

Each tool is a single file, single function. builtins.go is the registry.
This commit is contained in:
Levi Neely 2026-07-29 23:19:03 +02:00
parent 18beb15698
commit a21c256efa
9 changed files with 252 additions and 193 deletions

18
tools/builtins.go Normal file
View File

@ -0,0 +1,18 @@
// Package tools provides the built-in tool handlers for the toolsrv server.
// Each file in this package is one tool. Builtins() is the single registry.
package tools
import "ollie/toolsrv"
// Builtins returns the default set of built-in tool handlers.
func Builtins() map[string]toolsrv.Handler {
return map[string]toolsrv.Handler{
"shell": Shell,
"tool_list": ToolList,
"tool_load": ToolLoad,
"tool_active": ToolActive,
"skill_list": SkillList,
"skill_load": SkillLoad,
"skill_active": SkillActive,
}
}

38
tools/shell.go Normal file
View File

@ -0,0 +1,38 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"ollie/toolsrv"
)
// Shell executes a single bash command in a sandboxed environment.
func Shell(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
var a struct {
Cmd string `json:"cmd"`
Timeout int `json:"timeout"`
Sandbox string `json:"sandbox"`
Elevated bool `json:"elevated"`
Detach bool `json:"detach"`
}
if err := json.Unmarshal(args, &a); err != nil {
return "", fmt.Errorf("shell: bad args: %w", err)
}
if a.Cmd == "" {
return "", fmt.Errorf("shell: cmd is required")
}
timeout := a.Timeout
if timeout <= 0 {
timeout = 30
}
if a.Elevated {
return srv.ExecuteElevated(ctx, a.Cmd, srv.CWD(), timeout, a.Detach)
}
sandboxName := a.Sandbox
if sandboxName == "" {
sandboxName = "default"
}
return srv.ExecuteInSandbox(ctx, a.Cmd, "bash", timeout, sandboxName, false, "", a.Detach)
}

32
tools/skill_active.go Normal file
View File

@ -0,0 +1,32 @@
package tools
import (
"context"
"encoding/json"
"strings"
"ollie/toolsrv"
)
// SkillActive lists skills currently loaded in this session.
func SkillActive(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
reg := srv.SkillsRegistry()
sid := srv.SessionID()
if reg == nil || sid == "" {
return "(no skills loaded)", nil
}
loaded := reg.Loaded(sid)
if len(loaded) == 0 {
return "(no skills loaded)", nil
}
var out strings.Builder
for _, s := range loaded {
out.WriteString(s.Name)
if s.Description != "" {
out.WriteString(" — ")
out.WriteString(s.Description)
}
out.WriteString("\n")
}
return strings.TrimRight(out.String(), "\n"), nil
}

32
tools/skill_list.go Normal file
View File

@ -0,0 +1,32 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
"ollie/toolsrv"
)
// SkillList lists all available skills from the global registry.
func SkillList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
reg := srv.SkillsRegistry()
if reg == nil {
return "", fmt.Errorf("skill_list: no skills registry available")
}
summaries := reg.Summaries()
if len(summaries) == 0 {
return "(no skills found)", nil
}
var out strings.Builder
for _, s := range summaries {
out.WriteString(s.Name)
if s.Description != "" {
out.WriteString(" — ")
out.WriteString(s.Description)
}
out.WriteString("\n")
}
return strings.TrimRight(out.String(), "\n"), nil
}

37
tools/skill_load.go Normal file
View File

@ -0,0 +1,37 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"ollie/toolsrv"
)
// SkillLoad loads a skill into the current session.
func SkillLoad(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
var a struct {
Name string `json:"name"`
}
if err := json.Unmarshal(args, &a); err != nil {
return "", fmt.Errorf("skill_load: bad args: %w", err)
}
if a.Name == "" {
return "", fmt.Errorf("skill_load: name is required")
}
reg := srv.SkillsRegistry()
sid := srv.SessionID()
if reg == nil || sid == "" {
return "", fmt.Errorf("skill_load: no session registry")
}
if err := reg.Load(sid, a.Name); err != nil {
return "", fmt.Errorf("skill_load: %w", err)
}
content, err := reg.SkillContent(a.Name)
if err != nil {
return "", fmt.Errorf("skill_load: %w", err)
}
injectMsg := fmt.Sprintf("[system: loaded skill %s]\n\n%s", a.Name, content)
srv.InjectContent(injectMsg)
return fmt.Sprintf("loaded: %s (%d bytes)", a.Name, len(content)), nil
}

32
tools/tool_active.go Normal file
View File

@ -0,0 +1,32 @@
package tools
import (
"context"
"encoding/json"
"strings"
"ollie/toolsrv"
)
// ToolActive lists tools currently loaded (promoted) in this session.
func ToolActive(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
reg := srv.ToolRegistry()
sid := srv.SessionID()
if reg == nil || sid == "" {
return "(no tools loaded)", nil
}
loaded := reg.Loaded(sid)
if len(loaded) == 0 {
return "(no tools loaded)", nil
}
var out strings.Builder
for _, t := range loaded {
out.WriteString(t.Name)
if t.Description != "" {
out.WriteString(" — ")
out.WriteString(t.Description)
}
out.WriteString("\n")
}
return strings.TrimRight(out.String(), "\n"), nil
}

32
tools/tool_list.go Normal file
View File

@ -0,0 +1,32 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
"ollie/toolsrv"
)
// ToolList lists all available tools from the global registry.
func ToolList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
reg := srv.ToolRegistry()
if reg == nil {
return "", fmt.Errorf("tool_list: no registry available")
}
summaries := reg.Summaries()
if len(summaries) == 0 {
return "(no tools found)", nil
}
var out strings.Builder
for _, s := range summaries {
out.WriteString(s.Name)
if s.Description != "" {
out.WriteString(" — ")
out.WriteString(s.Description)
}
out.WriteString("\n")
}
return strings.TrimRight(out.String(), "\n"), nil
}

31
tools/tool_load.go Normal file
View File

@ -0,0 +1,31 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"ollie/toolsrv"
)
// ToolLoad loads a tool into the current session.
func ToolLoad(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
var a struct {
Name string `json:"name"`
}
if err := json.Unmarshal(args, &a); err != nil {
return "", fmt.Errorf("tool_load: bad args: %w", err)
}
if a.Name == "" {
return "", fmt.Errorf("tool_load: name is required")
}
reg := srv.ToolRegistry()
sid := srv.SessionID()
if reg == nil || sid == "" {
return "", fmt.Errorf("tool_load: no session registry")
}
if err := reg.Load(sid, a.Name); err != nil {
return "", fmt.Errorf("tool_load: %w", err)
}
return fmt.Sprintf("loaded: %s", a.Name), nil
}

View File

@ -1,193 +0,0 @@
// Package tools provides the built-in tool handlers for the toolsrv server.
package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
"ollie/toolsrv"
)
// Builtins returns the default set of built-in tool handlers.
func Builtins() map[string]toolsrv.Handler {
return map[string]toolsrv.Handler{
"shell": Shell,
"tool_list": ToolList,
"tool_load": ToolLoad,
"tool_active": ToolActive,
"skill_list": SkillList,
"skill_load": SkillLoad,
"skill_active": SkillActive,
}
}
// Shell executes a single bash command in a sandboxed environment.
func Shell(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
var a struct {
Cmd string `json:"cmd"`
Timeout int `json:"timeout"`
Sandbox string `json:"sandbox"`
Elevated bool `json:"elevated"`
Detach bool `json:"detach"`
}
if err := json.Unmarshal(args, &a); err != nil {
return "", fmt.Errorf("shell: bad args: %w", err)
}
if a.Cmd == "" {
return "", fmt.Errorf("shell: cmd is required")
}
timeout := a.Timeout
if timeout <= 0 {
timeout = 30
}
if a.Elevated {
return srv.ExecuteElevated(ctx, a.Cmd, srv.CWD(), timeout, a.Detach)
}
sandboxName := a.Sandbox
if sandboxName == "" {
sandboxName = "default"
}
return srv.ExecuteInSandbox(ctx, a.Cmd, "bash", timeout, sandboxName, false, "", a.Detach)
}
// ToolList lists all available tools from the global registry.
func ToolList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
reg := srv.ToolRegistry()
if reg == nil {
return "", fmt.Errorf("tool_list: no registry available")
}
summaries := reg.Summaries()
if len(summaries) == 0 {
return "(no tools found)", nil
}
var out strings.Builder
for _, s := range summaries {
out.WriteString(s.Name)
if s.Description != "" {
out.WriteString(" — ")
out.WriteString(s.Description)
}
out.WriteString("\n")
}
return strings.TrimRight(out.String(), "\n"), nil
}
// ToolLoad loads a tool into the current session.
func ToolLoad(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
var a struct {
Name string `json:"name"`
}
if err := json.Unmarshal(args, &a); err != nil {
return "", fmt.Errorf("tool_load: bad args: %w", err)
}
if a.Name == "" {
return "", fmt.Errorf("tool_load: name is required")
}
reg := srv.ToolRegistry()
sid := srv.SessionID()
if reg == nil || sid == "" {
return "", fmt.Errorf("tool_load: no session registry")
}
if err := reg.Load(sid, a.Name); err != nil {
return "", fmt.Errorf("tool_load: %w", err)
}
return fmt.Sprintf("loaded: %s", a.Name), nil
}
// ToolActive lists tools currently loaded (promoted) in this session.
func ToolActive(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
reg := srv.ToolRegistry()
sid := srv.SessionID()
if reg == nil || sid == "" {
return "(no tools loaded)", nil
}
loaded := reg.Loaded(sid)
if len(loaded) == 0 {
return "(no tools loaded)", nil
}
var out strings.Builder
for _, t := range loaded {
out.WriteString(t.Name)
if t.Description != "" {
out.WriteString(" — ")
out.WriteString(t.Description)
}
out.WriteString("\n")
}
return strings.TrimRight(out.String(), "\n"), nil
}
// SkillList lists all available skills from the global registry.
func SkillList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
reg := srv.SkillsRegistry()
if reg == nil {
return "", fmt.Errorf("skill_list: no skills registry available")
}
summaries := reg.Summaries()
if len(summaries) == 0 {
return "(no skills found)", nil
}
var out strings.Builder
for _, s := range summaries {
out.WriteString(s.Name)
if s.Description != "" {
out.WriteString(" — ")
out.WriteString(s.Description)
}
out.WriteString("\n")
}
return strings.TrimRight(out.String(), "\n"), nil
}
// SkillLoad loads a skill into the current session.
func SkillLoad(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
var a struct {
Name string `json:"name"`
}
if err := json.Unmarshal(args, &a); err != nil {
return "", fmt.Errorf("skill_load: bad args: %w", err)
}
if a.Name == "" {
return "", fmt.Errorf("skill_load: name is required")
}
reg := srv.SkillsRegistry()
sid := srv.SessionID()
if reg == nil || sid == "" {
return "", fmt.Errorf("skill_load: no session registry")
}
if err := reg.Load(sid, a.Name); err != nil {
return "", fmt.Errorf("skill_load: %w", err)
}
content, err := reg.SkillContent(a.Name)
if err != nil {
return "", fmt.Errorf("skill_load: %w", err)
}
injectMsg := fmt.Sprintf("[system: loaded skill %s]\n\n%s", a.Name, content)
srv.InjectContent(injectMsg)
return fmt.Sprintf("loaded: %s (%d bytes)", a.Name, len(content)), nil
}
// SkillActive lists skills currently loaded in this session.
func SkillActive(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
reg := srv.SkillsRegistry()
sid := srv.SessionID()
if reg == nil || sid == "" {
return "(no skills loaded)", nil
}
loaded := reg.Loaded(sid)
if len(loaded) == 0 {
return "(no skills loaded)", nil
}
var out strings.Builder
for _, s := range loaded {
out.WriteString(s.Name)
if s.Description != "" {
out.WriteString(" — ")
out.WriteString(s.Description)
}
out.WriteString("\n")
}
return strings.TrimRight(out.String(), "\n"), nil
}