From 81933e5281552fce425785adf94dbcb4fda924cf Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Sun, 16 Aug 2026 17:22:59 +0200 Subject: [PATCH] refactor toolsrv into client protocol and metadata packages --- cmd/ollie-9p/main.go | 3 +- cmd/olliesrv/bypass_notify.go | 14 ++-- cmd/olliesrv/internal/agent/agent.go | 10 +-- cmd/olliesrv/internal/agent/agent_config.go | 12 +-- cmd/olliesrv/internal/agent/agent_test.go | 4 +- cmd/olliesrv/internal/agent/history_test.go | 2 +- cmd/olliesrv/internal/agent/loop.go | 4 +- cmd/olliesrv/internal/agent/loop_test.go | 8 +- cmd/olliesrv/internal/agent/runtime.go | 23 +++--- cmd/olliesrv/internal/agent/runtime_test.go | 8 +- cmd/olliesrv/internal/agent/turn.go | 4 +- cmd/olliesrv/internal/backend/gemini.go | 1 - cmd/olliesrv/internal/backend/kiro.go | 3 - cmd/olliesrv/internal/backend/openai.go | 1 - .../internal/backend/openai_encode_test.go | 2 +- cmd/olliesrv/internal/bypass/broker.go | 4 +- cmd/olliesrv/internal/fs/newroot.go | 24 +++--- cmd/olliesrv/internal/fs/support.go | 4 +- cmd/olliesrv/internal/session/session.go | 21 +++--- cmd/olliesrv/internal/session/setup.go | 24 +++--- cmd/olliesrv/internal/toolclient/spawn.go | 4 +- cmd/olliesrv/main.go | 5 +- cmd/olliesrv/server.go | 42 ++++++++--- cmd/toolsrv/integration_test.go | 72 +++++++++--------- cmd/toolsrv/internal/bypass/bypass.go | 2 +- cmd/toolsrv/internal/exec/exec.go | 17 +++-- cmd/toolsrv/internal/registry/registry.go | 25 ++++--- cmd/toolsrv/internal/server/proc.go | 22 +++--- cmd/toolsrv/internal/server/proc_test.go | 16 ++-- cmd/toolsrv/internal/server/server.go | 14 ++-- cmd/toolsrv/main.go | 24 +++--- cmd/toolsrv/p9.go | 2 +- toolsrv/{client9p.go => client/client.go} | 28 +++---- toolsrv/metadata/discover.go | 48 ++++++++++++ toolsrv/{ => metadata}/meta.go | 9 ++- toolsrv/{parse.go => protocol/payload.go} | 2 +- toolsrv/protocol/types.go | 39 ++++++++++ toolsrv/registry.go | 74 ------------------- toolsrv/types.go | 16 ---- 39 files changed, 326 insertions(+), 311 deletions(-) rename toolsrv/{client9p.go => client/client.go} (96%) create mode 100644 toolsrv/metadata/discover.go rename toolsrv/{ => metadata}/meta.go (97%) rename toolsrv/{parse.go => protocol/payload.go} (98%) create mode 100644 toolsrv/protocol/types.go delete mode 100644 toolsrv/registry.go delete mode 100644 toolsrv/types.go diff --git a/cmd/ollie-9p/main.go b/cmd/ollie-9p/main.go index 9f910df..7ebd46a 100644 --- a/cmd/ollie-9p/main.go +++ b/cmd/ollie-9p/main.go @@ -27,8 +27,9 @@ import ( "text/tabwriter" "time" - "9fans.net/go/plan9" p9client "ollie/lib9p/client" + + "9fans.net/go/plan9" ) var ( diff --git a/cmd/olliesrv/bypass_notify.go b/cmd/olliesrv/bypass_notify.go index f6de01b..052888c 100644 --- a/cmd/olliesrv/bypass_notify.go +++ b/cmd/olliesrv/bypass_notify.go @@ -4,12 +4,14 @@ import ( "fmt" "sync" + toolsrvclient "ollie/toolsrv/client" + "ollie/toolsrv/protocol" + "github.com/godbus/dbus/v5" - "ollie/toolsrv" ) // bypassNotifier handles desktop notifications for bypass requests. -// When a notification action is clicked, it writes the resolution directly to toolsrv. +// When a notification action is clicked, it writes the resolution directly to toolsrvclient. type bypassNotifier struct { conn *dbus.Conn @@ -23,7 +25,7 @@ type pendingRequest struct { } type sessionRef struct { - dialFn func() *toolsrv.Conn + dialFn func() *toolsrvclient.Conn } var notifier *bypassNotifier @@ -50,15 +52,15 @@ func initBypassNotifier(conn *dbus.Conn) { } // notifyBypass sends a desktop notification for a bypass request. -// When the user responds, it writes the resolution directly to toolsrv. -func notifyBypass(req *toolsrv.BypassRequest, sessionID string, dialFn func() *toolsrv.Conn) { +// When the user responds, it writes the resolution directly to toolsrvclient. +func notifyBypass(req *protocol.BypassRequest, sessionID string, dialFn func() *toolsrvclient.Conn) { if notifier == nil { return } notifier.sendNotification(req, sessionID, dialFn) } -func (n *bypassNotifier) sendNotification(req *toolsrv.BypassRequest, sessionID string, dialFn func() *toolsrv.Conn) { +func (n *bypassNotifier) sendNotification(req *protocol.BypassRequest, sessionID string, dialFn func() *toolsrvclient.Conn) { obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") summary := "Elevation Request" diff --git a/cmd/olliesrv/internal/agent/agent.go b/cmd/olliesrv/internal/agent/agent.go index 4ec158f..0428330 100644 --- a/cmd/olliesrv/internal/agent/agent.go +++ b/cmd/olliesrv/internal/agent/agent.go @@ -13,7 +13,7 @@ import ( lib9p "ollie/lib9p/client" olog "ollie/log" "ollie/paths" - "ollie/toolsrv" + toolsrvclient "ollie/toolsrv/client" ) // Agent holds the state of the current Agent entity (the "agent" @@ -28,7 +28,7 @@ type Agent struct { agentsDir string systemPrompt string // system prompt for /agent reloads envBlock string // environment block for /agent reloads - newToolServer func() *toolsrv.Conn + newToolServer func() *toolsrvclient.Conn newBackend func(string) (backend.Backend, error) currentAction atomic.Pointer[actionHandle] warnedContext bool @@ -331,7 +331,7 @@ func (ag *Agent) emit(ev Event) { // SetToolServer updates the tool server connection and factory. // Used when resuming a paused session that was restored without infra. -func (ag *Agent) SetToolServer(newToolServer func() *toolsrv.Conn, conn *toolsrv.Conn) { +func (ag *Agent) SetToolServer(newToolServer func() *toolsrvclient.Conn, conn *toolsrvclient.Conn) { ag.newToolServer = newToolServer if ag.runtime != nil { ag.runtime.ToolServer = conn @@ -623,7 +623,7 @@ func (ag *Agent) ListModels() []string { // ToolServer returns the tool execution server, or nil if unavailable. // Exported for use by the 9P filesystem layer to sync tool registries. -func (ag *Agent) ToolServer() *toolsrv.Conn { +func (ag *Agent) ToolServer() *toolsrvclient.Conn { return ag.runtime.ToolServer } @@ -688,7 +688,7 @@ type AgentParams struct { Cwd string // working directory for tool execution SystemPrompt string EnvBlock string - NewToolServer func() *toolsrv.Conn + NewToolServer func() *toolsrvclient.Conn NewBackend func(string) (backend.Backend, error) Log *olog.Logger StartupMsgs []string diff --git a/cmd/olliesrv/internal/agent/agent_config.go b/cmd/olliesrv/internal/agent/agent_config.go index 501b69e..49405e5 100644 --- a/cmd/olliesrv/internal/agent/agent_config.go +++ b/cmd/olliesrv/internal/agent/agent_config.go @@ -23,13 +23,13 @@ func (p *Prompt) UnmarshalJSON(data []byte) error { } type AgentConfig struct { - Prompt Prompt `json:"prompt,omitempty"` - UserPrompts Prompt `json:"userPrompts,omitempty"` - Backend string `json:"backend,omitempty"` - Model string `json:"model,omitempty"` - Tools *bool `json:"tools,omitempty"` + Prompt Prompt `json:"prompt,omitempty"` + UserPrompts Prompt `json:"userPrompts,omitempty"` + Backend string `json:"backend,omitempty"` + Model string `json:"model,omitempty"` + Tools *bool `json:"tools,omitempty"` - AutoLoad []string `json:"autoLoad,omitempty"` + AutoLoad []string `json:"autoLoad,omitempty"` // MaxSteps caps the number of tool-call rounds per turn. 0 means unlimited. MaxSteps int `json:"maxSteps,omitempty"` CompactionModel string `json:"compactionModel,omitempty"` diff --git a/cmd/olliesrv/internal/agent/agent_test.go b/cmd/olliesrv/internal/agent/agent_test.go index c87315b..361fb70 100644 --- a/cmd/olliesrv/internal/agent/agent_test.go +++ b/cmd/olliesrv/internal/agent/agent_test.go @@ -4,11 +4,11 @@ import ( "strings" "testing" - "ollie/toolsrv" + "ollie/toolsrv/protocol" ) func TestRefreshToolListing(t *testing.T) { - infos := []toolsrv.ToolInfo{ + infos := []protocol.ToolInfo{ { Name: "new_tool", Description: "New description", diff --git a/cmd/olliesrv/internal/agent/history_test.go b/cmd/olliesrv/internal/agent/history_test.go index 26a0333..8fbd576 100644 --- a/cmd/olliesrv/internal/agent/history_test.go +++ b/cmd/olliesrv/internal/agent/history_test.go @@ -26,7 +26,7 @@ func TestBuildCompactedHistory_OrphanedToolWalkback(t *testing.T) { {Role: "assistant", Content: "old response 2"}, {Role: "user", Content: "request tool use"}, {Role: "assistant", Content: "", ToolCalls: []backend.ToolCall{{ID: "tc1", Name: "shell"}}}, // index 5 - {Role: "tool", ToolCallID: "tc1", Content: "command output"}, // index 6 - naive hotStart lands here! + {Role: "tool", ToolCallID: "tc1", Content: "command output"}, // index 6 - naive hotStart lands here! {Role: "user", Content: "next question"}, {Role: "assistant", Content: "answer"}, {Role: "user", Content: "another question"}, diff --git a/cmd/olliesrv/internal/agent/loop.go b/cmd/olliesrv/internal/agent/loop.go index b96b1c1..4e835f6 100644 --- a/cmd/olliesrv/internal/agent/loop.go +++ b/cmd/olliesrv/internal/agent/loop.go @@ -12,7 +12,7 @@ import ( "ollie/cmd/olliesrv/internal/backend" "ollie/paths" - "ollie/toolsrv" + toolsrvclient "ollie/toolsrv/client" ) const maxTransientRetries = 3 @@ -599,7 +599,7 @@ func (ag *Agent) execOne(ctx context.Context, call backend.ToolCall) (toolResult ag.emit(Event{Role: "tool", Name: call.Name, Content: result, OutputFormat: toolOutputFormat(rt, call.Name)}) return toolResult{ToolCallID: call.ID, Name: call.Name, Content: result, IsError: true}, true } - var rlErr *toolsrv.RateLimitedError + var rlErr *toolsrvclient.RateLimitedError if errors.As(err, &rlErr) { result = fmt.Sprintf("error: shell is rate-limited — blocked for %v. Do not call shell() again until the block expires. Use other tools or wait.", rlErr.Remaining) } else { diff --git a/cmd/olliesrv/internal/agent/loop_test.go b/cmd/olliesrv/internal/agent/loop_test.go index 1891298..13ce4ac 100644 --- a/cmd/olliesrv/internal/agent/loop_test.go +++ b/cmd/olliesrv/internal/agent/loop_test.go @@ -5,12 +5,12 @@ import ( "testing" "ollie/cmd/olliesrv/internal/backend" - "ollie/toolsrv" + "ollie/toolsrv/protocol" ) func TestToolConflictKeys(t *testing.T) { rt := &Runtime{ - ToolMeta: map[string]toolsrv.ToolInfo{ + ToolMeta: map[string]protocol.ToolInfo{ "file_read": {Name: "file_read", Scope: "read"}, "file_grep": {Name: "file_grep", Scope: "read"}, "file_edit": {Name: "file_edit", Scope: "write"}, @@ -80,7 +80,7 @@ func TestExecToolCallsBatching(t *testing.T) { // Verify the batching logic groups correctly without actually executing. // We test by inspecting which calls would be grouped together. rt := &Runtime{ - ToolMeta: map[string]toolsrv.ToolInfo{ + ToolMeta: map[string]protocol.ToolInfo{ "file_read": {Name: "file_read", Scope: "read"}, "file_edit": {Name: "file_edit", Scope: "write"}, "file_write": {Name: "file_write", Scope: "write"}, @@ -144,7 +144,7 @@ func TestExecToolCallsBatching(t *testing.T) { func TestExecToolCallsSamePathSerializes(t *testing.T) { rt := &Runtime{ - ToolMeta: map[string]toolsrv.ToolInfo{ + ToolMeta: map[string]protocol.ToolInfo{ "file_edit": {Name: "file_edit", Scope: "write"}, }, } diff --git a/cmd/olliesrv/internal/agent/runtime.go b/cmd/olliesrv/internal/agent/runtime.go index 932ed32..a362def 100644 --- a/cmd/olliesrv/internal/agent/runtime.go +++ b/cmd/olliesrv/internal/agent/runtime.go @@ -10,7 +10,8 @@ import ( "ollie/cmd/olliesrv/internal/backend" "ollie/cmd/olliesrv/internal/prompts" - "ollie/toolsrv" + toolsrvclient "ollie/toolsrv/client" + "ollie/toolsrv/protocol" ) // --- Preamble sections --- @@ -92,8 +93,8 @@ func EnvironmentBlock(cwd, platform string, isGitRepo bool, sandbox string) stri // renderTools produces a single combined section: a one-line summary per tool // followed immediately by its documentation (if any). This keeps the listing // and usage instructions adjacent so models don't lose context between them. -func RenderTools(infos []toolsrv.ToolInfo) string { - infos = append([]toolsrv.ToolInfo(nil), infos...) +func RenderTools(infos []protocol.ToolInfo) string { + infos = append([]protocol.ToolInfo(nil), infos...) sort.SliceStable(infos, func(i, j int) bool { if infos[i].Name == infos[j].Name { return infos[i].Server < infos[j].Server @@ -138,10 +139,10 @@ func RenderTools(infos []toolsrv.ToolInfo) string { // agents replaces it atomically. type Runtime struct { Backend backend.Backend - ToolServer *toolsrv.Conn + ToolServer *toolsrvclient.Conn Preamble *Preamble Tools []backend.Tool - ToolMeta map[string]toolsrv.ToolInfo + ToolMeta map[string]protocol.ToolInfo Exec toolExecutor GenParams backend.GenerationParams MaxSteps int @@ -160,10 +161,10 @@ func (rt *Runtime) PreambleString() string { // BuildRuntime constructs a Runtime from a pre-configured tool server and // optional agent config. -func BuildRuntime(cfg *AgentConfig, srv *toolsrv.Conn, cwd string, env []string, systemPrompt, envBlock string) *Runtime { +func BuildRuntime(cfg *AgentConfig, srv *toolsrvclient.Conn, cwd string, env []string, systemPrompt, envBlock string) *Runtime { var messages []string - var allToolInfos []toolsrv.ToolInfo + var allToolInfos []protocol.ToolInfo var allTools []backend.Tool if srv != nil && (cfg == nil || cfg.ToolsEnabled()) { @@ -210,7 +211,7 @@ func BuildRuntime(cfg *AgentConfig, srv *toolsrv.Conn, cwd string, env []string, return text, blocks, nil } - toolMeta := make(map[string]toolsrv.ToolInfo, len(allToolInfos)) + toolMeta := make(map[string]protocol.ToolInfo, len(allToolInfos)) for _, ti := range allToolInfos { toolMeta[ti.Name] = ti } @@ -250,7 +251,7 @@ func ResolveSystemPrompt(cfg *AgentConfig) string { return prompts.DefaultSystemPrompt } -func toolInfosToBackend(infos []toolsrv.ToolInfo) []backend.Tool { +func toolInfosToBackend(infos []protocol.ToolInfo) []backend.Tool { out := make([]backend.Tool, len(infos)) for i, t := range infos { out[i] = backend.Tool{ @@ -308,9 +309,9 @@ func injectDispatchFlags(schema json.RawMessage) json.RawMessage { // extractToolResult parses the tool server's JSON-RPC response into text // and optional content blocks. // -// See toolsrv.ToolResult for the wire format. +// See protocol.ToolResult for the wire format. func extractToolResult(raw json.RawMessage) (text string, contentBlocks []backend.ContentBlock, isError bool) { - var result toolsrv.ToolResult + var result protocol.ToolResult if err := json.Unmarshal(raw, &result); err != nil { return string(raw), nil, false } diff --git a/cmd/olliesrv/internal/agent/runtime_test.go b/cmd/olliesrv/internal/agent/runtime_test.go index a110781..5cd7675 100644 --- a/cmd/olliesrv/internal/agent/runtime_test.go +++ b/cmd/olliesrv/internal/agent/runtime_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "ollie/toolsrv" + "ollie/toolsrv/protocol" ) func TestEnvironmentBlockStable(t *testing.T) { @@ -19,7 +19,7 @@ func TestEnvironmentBlockStable(t *testing.T) { } func TestPreambleSections(t *testing.T) { - toolInfos := []toolsrv.ToolInfo{ + toolInfos := []protocol.ToolInfo{ {Name: "file_read", Description: "Read a file with line numbers", Prompt: "**file_read**\n\nRead a file with line numbers...", InputSchema: []byte(`{"type":"object"}`)}, {Name: "file_write", Description: "Create or overwrite a file", Prompt: "**file_write**\n\nCreate or overwrite a file...", InputSchema: []byte(`{"type":"object"}`), Server: "remote"}, {Name: "shell", Description: "Execute shell command", InputSchema: []byte(`{}`)}, @@ -41,9 +41,9 @@ func TestPreambleSections(t *testing.T) { } func TestRenderToolsStableOrder(t *testing.T) { - infos := []toolsrv.ToolInfo{{Name: "z_tool", Description: "z"}, {Name: "a_tool", Description: "a"}} + infos := []protocol.ToolInfo{{Name: "z_tool", Description: "z"}, {Name: "a_tool", Description: "a"}} first := RenderTools(infos) - second := RenderTools([]toolsrv.ToolInfo{infos[1], infos[0]}) + second := RenderTools([]protocol.ToolInfo{infos[1], infos[0]}) if first != second || strings.Index(first, "a_tool") > strings.Index(first, "z_tool") { t.Fatalf("tool rendering is not deterministic: %q", first) } diff --git a/cmd/olliesrv/internal/agent/turn.go b/cmd/olliesrv/internal/agent/turn.go index 011daf8..f6f1e8f 100644 --- a/cmd/olliesrv/internal/agent/turn.go +++ b/cmd/olliesrv/internal/agent/turn.go @@ -10,7 +10,7 @@ import ( "ollie/cmd/olliesrv/internal/backend" "ollie/cmd/olliesrv/internal/metrics" - "ollie/toolsrv" + "ollie/toolsrv/protocol" ) // Submit processes one line of user input: it starts an agent turn that streams @@ -131,7 +131,7 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string { return "" } ag.runtime.Tools = toolInfosToBackend(ti) - meta := make(map[string]toolsrv.ToolInfo, len(ti)) + meta := make(map[string]protocol.ToolInfo, len(ti)) for _, info := range ti { meta[info.Name] = info } diff --git a/cmd/olliesrv/internal/backend/gemini.go b/cmd/olliesrv/internal/backend/gemini.go index b0f4e01..0d36392 100644 --- a/cmd/olliesrv/internal/backend/gemini.go +++ b/cmd/olliesrv/internal/backend/gemini.go @@ -16,4 +16,3 @@ func NewGemini(apiKey string) (*OpenAIBackend, error) { b.model = geminiDefaultModel return b, nil } - diff --git a/cmd/olliesrv/internal/backend/kiro.go b/cmd/olliesrv/internal/backend/kiro.go index 8c20479..3361496 100644 --- a/cmd/olliesrv/internal/backend/kiro.go +++ b/cmd/olliesrv/internal/backend/kiro.go @@ -1078,7 +1078,6 @@ func kiroRandomJitter() time.Duration { return time.Duration(n%uint32(kiroTokenExpiryJitter/time.Millisecond)) * time.Millisecond } - // ── SQLite helpers ──────────────────────────────────────────────────────────── // Kiro supports two auth flows whose tokens live under different SQLite keys: @@ -1474,5 +1473,3 @@ func newKiroAuthSource(apiKey string) (kiroAuthSource, error) { func kiroDefaultSQLiteKey() string { return "sqlite://" + filepath.Join(paths.UserDataHome(), "kiro-cli", "data.sqlite3") } - - diff --git a/cmd/olliesrv/internal/backend/openai.go b/cmd/olliesrv/internal/backend/openai.go index 92608db..975f64b 100644 --- a/cmd/olliesrv/internal/backend/openai.go +++ b/cmd/olliesrv/internal/backend/openai.go @@ -544,4 +544,3 @@ func (b *OpenAIBackend) ChatStream(ctx context.Context, messages []Message, tool return streamRequest(b.client, httpReq, "openai", streamOpenAISSE) } - diff --git a/cmd/olliesrv/internal/backend/openai_encode_test.go b/cmd/olliesrv/internal/backend/openai_encode_test.go index 6b735d9..bfcbd34 100644 --- a/cmd/olliesrv/internal/backend/openai_encode_test.go +++ b/cmd/olliesrv/internal/backend/openai_encode_test.go @@ -318,7 +318,7 @@ func TestOpenRouterClaude_LastToolCacheControl(t *testing.T) { } // TestOpenRouterNonClaude_NoCacheControl verifies that a non-Claude model on -// openrouter does NOT get cache_control injected into system messages or toolsrv. +// openrouter does NOT get cache_control injected into system messages or toolsrvclient. func TestOpenRouterNonClaude_NoCacheControl(t *testing.T) { top := captureRawRequest(t, "openrouter", "mistral-7b", []Message{{Role: "system", Content: "be helpful"}, {Role: "user", Content: "hi"}}, diff --git a/cmd/olliesrv/internal/bypass/broker.go b/cmd/olliesrv/internal/bypass/broker.go index 1725ca2..40149c7 100644 --- a/cmd/olliesrv/internal/bypass/broker.go +++ b/cmd/olliesrv/internal/bypass/broker.go @@ -11,12 +11,12 @@ import ( const ( RequestTTL = 300 * time.Second - BurstLimit = 3 // max denied/unanswered requests in burst window + BurstLimit = 3 // max denied/unanswered requests in burst window RefillInterval = 10 * time.Second // one slot recovers every 10s (30s to full) ) // Broker manages bypass request evaluation and policy. -// It does NOT execute commands - that happens in toolsrv. +// It does NOT execute commands - that happens in toolsrvclient. type Broker struct { policy *PolicyStore notify NotifyFunc diff --git a/cmd/olliesrv/internal/fs/newroot.go b/cmd/olliesrv/internal/fs/newroot.go index b7574b4..044d360 100644 --- a/cmd/olliesrv/internal/fs/newroot.go +++ b/cmd/olliesrv/internal/fs/newroot.go @@ -3,24 +3,24 @@ package fs import ( "context" - "ollie/virtfs" - olog "ollie/log" "ollie/cmd/olliesrv/internal/session" + olog "ollie/log" + "ollie/virtfs" ) // Config carries all runtime dependencies for the filesystem tree. // It is the single input to NewRoot. type Config struct { - Ctx context.Context - Log *olog.Logger - Sink *olog.Sink - AgentsDir string - SessionsDir string - Yolo bool - ModelCache *ModelCache - BypassNotify session.BypassNotifyFunc - Shutdown func() - Invalidate func() + Ctx context.Context + Log *olog.Logger + Sink *olog.Sink + AgentsDir string + SessionsDir string + Yolo bool + ModelCache *ModelCache + BypassNotify session.BypassNotifyFunc + Shutdown func() + Invalidate func() } // NewRoot creates the complete 9P filesystem tree from the spec. diff --git a/cmd/olliesrv/internal/fs/support.go b/cmd/olliesrv/internal/fs/support.go index e89fc90..95ab9ef 100644 --- a/cmd/olliesrv/internal/fs/support.go +++ b/cmd/olliesrv/internal/fs/support.go @@ -10,7 +10,7 @@ import ( "ollie/cmd/olliesrv/internal/agent" "ollie/cmd/olliesrv/internal/session" - "ollie/toolsrv" + "ollie/toolsrv/protocol" "ollie/virtfs" ) @@ -106,7 +106,7 @@ func parseAgentNewRequest(data []byte) (session.AgentParams, string, string) { continue } if k, v, ok := strings.Cut(tok, "="); ok { - v = toolsrv.UnescapeValue(v) + v = protocol.UnescapeValue(v) switch k { case "cwd": p.CWD = v diff --git a/cmd/olliesrv/internal/session/session.go b/cmd/olliesrv/internal/session/session.go index 9b47647..7180283 100644 --- a/cmd/olliesrv/internal/session/session.go +++ b/cmd/olliesrv/internal/session/session.go @@ -14,7 +14,8 @@ import ( "ollie/cmd/olliesrv/internal/toolclient" olog "ollie/log" "ollie/paths" - "ollie/toolsrv" + toolsrvclient "ollie/toolsrv/client" + "ollie/toolsrv/protocol" ) // Session owns all session-level state: identity, agents, tool server lifecycle. @@ -38,8 +39,8 @@ type Session struct { // Mutable state protected by mu mu sync.RWMutex - name string // friendly name - toolsConn *toolsrv.Conn // multiplexed RPC connection + name string // friendly name + toolsConn *toolsrvclient.Conn // multiplexed RPC connection paused bool // Autosave @@ -84,7 +85,7 @@ func (s *Session) SetName(name string) { } // ToolsConn returns the tool server connection. -func (s *Session) ToolsConn() *toolsrv.Conn { +func (s *Session) ToolsConn() *toolsrvclient.Conn { s.mu.RLock() defer s.mu.RUnlock() return s.toolsConn @@ -92,7 +93,7 @@ func (s *Session) ToolsConn() *toolsrv.Conn { // DialToolServer dials a fresh toolsrv connection via the keeper. // Caller is responsible for closing the returned conn. -func (s *Session) DialToolServer() *toolsrv.Conn { +func (s *Session) DialToolServer() *toolsrvclient.Conn { s.mu.RLock() k := s.Keeper s.mu.RUnlock() @@ -107,7 +108,7 @@ func (s *Session) DialToolServer() *toolsrv.Conn { } // SetToolsConn sets the tool server connection. -func (s *Session) SetToolsConn(conn *toolsrv.Conn) { +func (s *Session) SetToolsConn(conn *toolsrvclient.Conn) { s.mu.Lock() defer s.mu.Unlock() s.toolsConn = conn @@ -115,7 +116,7 @@ func (s *Session) SetToolsConn(conn *toolsrv.Conn) { // BypassNotifyFunc is called when a bypass request arrives. // It receives the request, session ID, and a dial function to get a connection for resolution. -type BypassNotifyFunc func(req *toolsrv.BypassRequest, sessionID string, dialFn func() *toolsrv.Conn) +type BypassNotifyFunc func(req *protocol.BypassRequest, sessionID string, dialFn func() *toolsrvclient.Conn) // StartBypassLoop starts a goroutine that reads bypass requests from toolsrv // and calls notifyFn for each one. This should be called after SetToolsConn. @@ -151,7 +152,7 @@ func (s *Session) runBypassLoop(notifyFn BypassNotifyFunc) { // Notify (non-blocking) - the notifier will write resolution when user responds if notifyFn != nil { - notifyFn(req, s.ID, s.DialToolServer) + notifyFn((*protocol.BypassRequest)(req), s.ID, s.DialToolServer) } } } @@ -326,7 +327,7 @@ func (s *Session) Resume() error { if err != nil { return fmt.Errorf("resume agent dial: %w", err) } - ag.SetToolServer(func() *toolsrv.Conn { + ag.SetToolServer(func() *toolsrvclient.Conn { c, _ := s.Keeper.Dial() return c }, agConn) @@ -392,7 +393,7 @@ func (s *Session) LoadTool(name string, ag *agent.Agent) error { } // LoadToolOnConn loads a tool on the given connection. -func LoadToolOnConn(conn *toolsrv.Conn, name string) error { +func LoadToolOnConn(conn *toolsrvclient.Conn, name string) error { if conn == nil { return nil } diff --git a/cmd/olliesrv/internal/session/setup.go b/cmd/olliesrv/internal/session/setup.go index bf108d5..c4db13b 100644 --- a/cmd/olliesrv/internal/session/setup.go +++ b/cmd/olliesrv/internal/session/setup.go @@ -5,10 +5,10 @@ import ( "fmt" "ollie/cmd/olliesrv/internal/agent" + "ollie/cmd/olliesrv/internal/prompts" "ollie/cmd/olliesrv/internal/toolclient" "ollie/paths" - "ollie/cmd/olliesrv/internal/prompts" - "ollie/toolsrv" + toolsrvclient "ollie/toolsrv/client" ) // SessionInfra holds the tool server infrastructure for a session. @@ -16,8 +16,8 @@ import ( type SessionInfra struct { Proc *toolclient.Process Keeper *toolclient.ProcessKeeper - ToolsConn *toolsrv.Conn - NewToolServer func() *toolsrv.Conn + ToolsConn *toolsrvclient.Conn + NewToolServer func() *toolsrvclient.Conn Platform string IsGitRepo bool } @@ -39,7 +39,7 @@ type ToolServerConfig struct { type InfraConfig struct { Proc *toolclient.Process Keeper *toolclient.ProcessKeeper - ToolsConn *toolsrv.Conn + ToolsConn *toolsrvclient.Conn } // SetupToolServer spawns the tool server process and creates the infrastructure. @@ -50,9 +50,9 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) { // Reuse existing infrastructure if provided. if cfg.ReuseFrom != nil { r := cfg.ReuseFrom - var newToolServer func() *toolsrv.Conn + var newToolServer func() *toolsrvclient.Conn if r.Keeper != nil { - newToolServer = func() *toolsrv.Conn { + newToolServer = func() *toolsrvclient.Conn { conn, err := r.Keeper.Dial() if err != nil { return nil @@ -60,7 +60,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) { return conn } } else { - newToolServer = func() *toolsrv.Conn { + newToolServer = func() *toolsrvclient.Conn { return r.ToolsConn } } @@ -79,7 +79,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) { } var proc *toolclient.Process var keeper *toolclient.ProcessKeeper - var newToolServer func() *toolsrv.Conn + var newToolServer func() *toolsrvclient.Conn var err error var platform string var isGitRepo bool @@ -102,7 +102,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) { Yolo: cfg.Yolo, }) }) - newToolServer = func() *toolsrv.Conn { + newToolServer = func() *toolsrvclient.Conn { conn, err := keeper.Dial() if err != nil { return nil @@ -126,7 +126,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) { keeper = toolclient.NewProcessKeeper(cfg.Ctx, proc, func(ctx context.Context) (*toolclient.Process, error) { return toolclient.Spawn(ctx, cfg.CWD, dialOpts...) }) - newToolServer = func() *toolsrv.Conn { + newToolServer = func() *toolsrvclient.Conn { conn, err := keeper.Dial() if err != nil { return nil @@ -173,7 +173,7 @@ func BuildPromptLayers(cfg *agent.AgentConfig, cwd, sessID, uname, platform stri // LoadAutoLoadTools loads all tools from the agent config's autoLoad list. // It first sends the session ID and uname to the tool server so that // ollie-remote can attach its tool registry before processing tool_load RPCs. -func LoadAutoLoadTools(cfg *agent.AgentConfig, conn *toolsrv.Conn, sessID, uname string, logError func(string, ...any)) { +func LoadAutoLoadTools(cfg *agent.AgentConfig, conn *toolsrvclient.Conn, sessID, uname string, logError func(string, ...any)) { if cfg == nil || conn == nil { return } diff --git a/cmd/olliesrv/internal/toolclient/spawn.go b/cmd/olliesrv/internal/toolclient/spawn.go index cdf382c..ef41f2a 100644 --- a/cmd/olliesrv/internal/toolclient/spawn.go +++ b/cmd/olliesrv/internal/toolclient/spawn.go @@ -1,4 +1,4 @@ -// spawn.go - Process spawning and lifecycle management for toolsrv. +// spawn.go - Process spawning and lifecycle management for toolsrvclient. package toolclient import ( @@ -18,7 +18,7 @@ import ( "time" "ollie/paths" - toolsrvclient "ollie/toolsrv" + toolsrvclient "ollie/toolsrv/client" ) // Process represents a running toolsrv process. diff --git a/cmd/olliesrv/main.go b/cmd/olliesrv/main.go index 580ecbc..88e33a6 100644 --- a/cmd/olliesrv/main.go +++ b/cmd/olliesrv/main.go @@ -11,13 +11,14 @@ import ( "path/filepath" "syscall" - "9fans.net/go/plan9/client" "ollie/cmd/olliesrv/internal/agent" - "ollie/env" "ollie/cmd/olliesrv/internal/fs" + "ollie/env" olog "ollie/log" "ollie/paths" + "9fans.net/go/plan9/client" + "github.com/godbus/dbus/v5" ) diff --git a/cmd/olliesrv/server.go b/cmd/olliesrv/server.go index f2a991d..80d4c3e 100644 --- a/cmd/olliesrv/server.go +++ b/cmd/olliesrv/server.go @@ -15,8 +15,8 @@ import ( "time" "ollie/cmd/olliesrv/internal/fs" - olog "ollie/log" "ollie/cmd/olliesrv/internal/session" + olog "ollie/log" "9fans.net/go/plan9" ) @@ -113,18 +113,36 @@ func New(cfg Config) *Server { } return &plan9.Fcall{Type: plan9.Rversion, Tag: fc.Tag, Msize: msize, Version: "9P2000"} }, - plan9.Tauth: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return errFcall(fc, "no auth required") }, - plan9.Tflush: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return flush(cs, fc) }, + plan9.Tauth: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { + return errFcall(fc, "no auth required") + }, + plan9.Tflush: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return flush(cs, fc) }, plan9.Tattach: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return attach(s.groups, cs, fc) }, - plan9.Twalk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return walk(cfg.RootTree, cs, fc) }, - plan9.Topen: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return open(cfg.RootTree, s.groups, cs, fc) }, - plan9.Tcreate: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return create(cfg.RootTree, cs, fc) }, - plan9.Tread: func(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan9.Fcall { return read(cfg.RootTree, cs, fc, ctx) }, - plan9.Twrite: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return write(cs, fc) }, - plan9.Tstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return stat(cfg.RootTree, cs, fc) }, - plan9.Twstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return wstat(cfg.RootTree, cs, fc) }, - plan9.Tclunk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return clunk(cfg.RootTree, cs, fc) }, - plan9.Tremove: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return remove(cfg.RootTree, cs, fc) }, + plan9.Twalk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { + return walk(cfg.RootTree, cs, fc) + }, + plan9.Topen: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { + return open(cfg.RootTree, s.groups, cs, fc) + }, + plan9.Tcreate: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { + return create(cfg.RootTree, cs, fc) + }, + plan9.Tread: func(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan9.Fcall { + return read(cfg.RootTree, cs, fc, ctx) + }, + plan9.Twrite: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return write(cs, fc) }, + plan9.Tstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { + return stat(cfg.RootTree, cs, fc) + }, + plan9.Twstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { + return wstat(cfg.RootTree, cs, fc) + }, + plan9.Tclunk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { + return clunk(cfg.RootTree, cs, fc) + }, + plan9.Tremove: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { + return remove(cfg.RootTree, cs, fc) + }, } return s } diff --git a/cmd/toolsrv/integration_test.go b/cmd/toolsrv/integration_test.go index e63b0b4..cea1d9e 100644 --- a/cmd/toolsrv/integration_test.go +++ b/cmd/toolsrv/integration_test.go @@ -15,7 +15,9 @@ import ( "testing" "time" - "ollie/toolsrv" + toolsrvclient "ollie/toolsrv/client" + "ollie/toolsrv/metadata" + "ollie/toolsrv/protocol" ) var testToolsrvBinary string @@ -85,7 +87,7 @@ network: } // Symlink the real tools directory so tests can find them - realToolsPath := toolsrv.ToolsPath() + realToolsPath := metadata.ToolsPath() if _, err := os.Stat(realToolsPath); err == nil { if err := os.Symlink(realToolsPath, filepath.Join(cfgDir, "tools")); err != nil { os.RemoveAll(tmpDir) @@ -93,7 +95,7 @@ network: } } - socketPath = filepath.Join(tmpDir, "toolsrv.sock") + socketPath = filepath.Join(tmpDir, "toolsrvclient.sock") cwd, _ := os.Getwd() ctx, cancel := context.WithCancel(context.Background()) @@ -140,7 +142,7 @@ func TestIntegration_FirstConnectionSetsSecret(t *testing.T) { // First connection with a secret should succeed secret := "test-secret-first-connection" - conn, err := toolsrv.Dial(socketPath, secret) + conn, err := toolsrvclient.Dial(socketPath, secret) if err != nil { t.Fatalf("first Dial failed: %v", err) } @@ -164,7 +166,7 @@ func TestIntegration_SameSecretReconnects(t *testing.T) { secret := "test-secret-reconnect" // First connection establishes the secret - conn1, err := toolsrv.Dial(socketPath, secret) + conn1, err := toolsrvclient.Dial(socketPath, secret) if err != nil { t.Fatalf("first Dial failed: %v", err) } @@ -172,7 +174,7 @@ func TestIntegration_SameSecretReconnects(t *testing.T) { conn1.Close() // Second connection with same secret should succeed - conn2, err := toolsrv.Dial(socketPath, secret) + conn2, err := toolsrvclient.Dial(socketPath, secret) if err != nil { t.Fatalf("second Dial failed: %v", err) } @@ -189,14 +191,14 @@ func TestIntegration_WrongSecretFails(t *testing.T) { defer cleanup() // First connection establishes the secret - conn1, err := toolsrv.Dial(socketPath, "correct-secret") + conn1, err := toolsrvclient.Dial(socketPath, "correct-secret") if err != nil { t.Fatalf("first Dial failed: %v", err) } conn1.Close() // Second connection with wrong secret should fail - conn2, err := toolsrv.Dial(socketPath, "wrong-secret") + conn2, err := toolsrvclient.Dial(socketPath, "wrong-secret") if err == nil { conn2.Close() t.Fatal("Dial with wrong secret should fail") @@ -208,7 +210,7 @@ func TestIntegration_EmptySecretGeneratesOne(t *testing.T) { defer cleanup() // Empty secret should generate a random one - conn, err := toolsrv.Dial(socketPath, "") + conn, err := toolsrvclient.Dial(socketPath, "") if err != nil { t.Fatalf("Dial with empty secret failed: %v", err) } @@ -229,7 +231,7 @@ func TestIntegration_BasicOperations(t *testing.T) { socketPath, cleanup := startTestServer(t) defer cleanup() - conn, err := toolsrv.Dial(socketPath, "test-secret-ops") + conn, err := toolsrvclient.Dial(socketPath, "test-secret-ops") if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -260,14 +262,14 @@ func TestIntegration_BasicOperations(t *testing.T) { func TestIntegration_ToolExecution(t *testing.T) { // Skip if tools directory doesn't exist (CI environment) - if _, err := os.ReadDir(toolsrv.ToolsPath()); err != nil { + if _, err := os.ReadDir(metadata.ToolsPath()); err != nil { t.Skipf("skipping: tools directory not available: %v", err) } socketPath, cleanup := startTestServer(t) defer cleanup() - conn, err := toolsrv.Dial(socketPath, "test-secret-exec") + conn, err := toolsrvclient.Dial(socketPath, "test-secret-exec") if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -304,14 +306,14 @@ func TestIntegration_ToolExecution(t *testing.T) { } // Parse the result the same way agent/runtime.go does - var toolResult toolsrv.ToolResult + var toolResult protocol.ToolResult if err := json.Unmarshal(result, &toolResult); err != nil { - t.Fatalf("failed to parse toolsrv.ToolResult: %v (raw: %s)", err, string(result)) + t.Fatalf("failed to parse protocol.ToolResult: %v (raw: %s)", err, string(result)) } // Verify structure if len(toolResult.Content) == 0 { - t.Fatal("toolsrv.ToolResult.Content is empty") + t.Fatal("protocol.ToolResult.Content is empty") } if toolResult.Content[0].Type != "text" { t.Errorf("expected type 'text', got %q", toolResult.Content[0].Type) @@ -320,7 +322,7 @@ func TestIntegration_ToolExecution(t *testing.T) { t.Errorf("output doesn't contain expected text: %q", toolResult.Content[0].Text) } if toolResult.IsError { - t.Error("toolsrv.ToolResult.IsError should be false") + t.Error("protocol.ToolResult.IsError should be false") } t.Logf("Tool result: %+v", toolResult) @@ -328,14 +330,14 @@ func TestIntegration_ToolExecution(t *testing.T) { func TestIntegration_MultilineContent(t *testing.T) { // Skip if tools directory doesn't exist (CI environment) - if _, err := os.ReadDir(toolsrv.ToolsPath()); err != nil { + if _, err := os.ReadDir(metadata.ToolsPath()); err != nil { t.Skipf("skipping: tools directory not available: %v", err) } socketPath, cleanup := startTestServer(t) defer cleanup() - conn, err := toolsrv.Dial(socketPath, "test-secret-multiline") + conn, err := toolsrvclient.Dial(socketPath, "test-secret-multiline") if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -360,9 +362,9 @@ func TestIntegration_MultilineContent(t *testing.T) { t.Fatalf("CallTool failed: %v", err) } - var toolResult toolsrv.ToolResult + var toolResult protocol.ToolResult if err := json.Unmarshal(result, &toolResult); err != nil { - t.Fatalf("failed to parse toolsrv.ToolResult: %v (raw: %s)", err, string(result)) + t.Fatalf("failed to parse protocol.ToolResult: %v (raw: %s)", err, string(result)) } if toolResult.IsError { @@ -382,14 +384,14 @@ func TestIntegration_MultilineContent(t *testing.T) { func TestIntegration_ToolExecutionError(t *testing.T) { // Skip if tools directory doesn't exist (CI environment) - if _, err := os.ReadDir(toolsrv.ToolsPath()); err != nil { + if _, err := os.ReadDir(metadata.ToolsPath()); err != nil { t.Skipf("skipping: tools directory not available: %v", err) } socketPath, cleanup := startTestServer(t) defer cleanup() - conn, err := toolsrv.Dial(socketPath, "test-secret-exec-err") + conn, err := toolsrvclient.Dial(socketPath, "test-secret-exec-err") if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -410,14 +412,14 @@ func TestIntegration_ToolExecutionError(t *testing.T) { } // Parse the result - var toolResult toolsrv.ToolResult + var toolResult protocol.ToolResult if err := json.Unmarshal(result, &toolResult); err != nil { - t.Fatalf("failed to parse toolsrv.ToolResult: %v (raw: %s)", err, string(result)) + t.Fatalf("failed to parse protocol.ToolResult: %v (raw: %s)", err, string(result)) } // Should be an error result with exit code info if len(toolResult.Content) == 0 { - t.Fatal("toolsrv.ToolResult.Content is empty") + t.Fatal("protocol.ToolResult.Content is empty") } if !toolResult.IsError { t.Error("expected IsError=true for non-zero exit") @@ -436,7 +438,7 @@ func TestIntegration_ConcurrentConnections(t *testing.T) { secret := "test-secret-concurrent" // Establish the secret - conn0, err := toolsrv.Dial(socketPath, secret) + conn0, err := toolsrvclient.Dial(socketPath, secret) if err != nil { t.Fatalf("initial Dial failed: %v", err) } @@ -445,11 +447,11 @@ func TestIntegration_ConcurrentConnections(t *testing.T) { // Open multiple concurrent connections const numConns = 5 - conns := make([]*toolsrv.Conn, numConns) + conns := make([]*toolsrvclient.Conn, numConns) errors := make([]error, numConns) for i := 0; i < numConns; i++ { - conns[i], errors[i] = toolsrv.Dial(socketPath, secret) + conns[i], errors[i] = toolsrvclient.Dial(socketPath, secret) } // Check all succeeded with same token @@ -478,7 +480,7 @@ func TestIntegration_ProcessKeeperRespawn(t *testing.T) { socketPath1, cleanup1 := startTestServer(t) secret1 := "secret-for-first-server" - conn1, err := toolsrv.Dial(socketPath1, secret1) + conn1, err := toolsrvclient.Dial(socketPath1, secret1) if err != nil { cleanup1() t.Fatalf("first Dial failed: %v", err) @@ -495,7 +497,7 @@ func TestIntegration_ProcessKeeperRespawn(t *testing.T) { // New server should accept a new secret (any secret, since it's fresh) secret2 := "secret-for-second-server" - conn2, err := toolsrv.Dial(socketPath2, secret2) + conn2, err := toolsrvclient.Dial(socketPath2, secret2) if err != nil { t.Fatalf("second Dial failed: %v", err) } @@ -508,7 +510,7 @@ func TestIntegration_ProcessKeeperRespawn(t *testing.T) { } // Old secret should NOT work on new server - conn3, err := toolsrv.Dial(socketPath2, secret1) + conn3, err := toolsrvclient.Dial(socketPath2, secret1) if err == nil { conn3.Close() t.Error("old secret should not work on new server") @@ -522,14 +524,14 @@ func TestIntegration_ProcessKeeperRespawn(t *testing.T) { // call causes the call to return quickly (not wait for the full tool execution). func TestIntegration_ToolCancellation(t *testing.T) { // Skip if tools directory doesn't exist (CI environment) - if _, err := os.ReadDir(toolsrv.ToolsPath()); err != nil { + if _, err := os.ReadDir(metadata.ToolsPath()); err != nil { t.Skipf("skipping: tools directory not available: %v", err) } socketPath, cleanup := startTestServer(t) defer cleanup() - conn, err := toolsrv.Dial(socketPath, "test-secret-cancel") + conn, err := toolsrvclient.Dial(socketPath, "test-secret-cancel") if err != nil { t.Fatalf("Dial failed: %v", err) } @@ -588,14 +590,14 @@ func TestIntegration_ToolCancellation(t *testing.T) { // take a long time to complete. func TestIntegration_ToolCancellation_ContextPropagation(t *testing.T) { // Skip if tools directory doesn't exist (CI environment) - if _, err := os.ReadDir(toolsrv.ToolsPath()); err != nil { + if _, err := os.ReadDir(metadata.ToolsPath()); err != nil { t.Skipf("skipping: tools directory not available: %v", err) } socketPath, cleanup := startTestServer(t) defer cleanup() - conn, err := toolsrv.Dial(socketPath, "test-secret-cancel-prop") + conn, err := toolsrvclient.Dial(socketPath, "test-secret-cancel-prop") if err != nil { t.Fatalf("Dial failed: %v", err) } diff --git a/cmd/toolsrv/internal/bypass/bypass.go b/cmd/toolsrv/internal/bypass/bypass.go index edba4fc..e070aed 100644 --- a/cmd/toolsrv/internal/bypass/bypass.go +++ b/cmd/toolsrv/internal/bypass/bypass.go @@ -1,4 +1,4 @@ -// Package bypass handles bypass request submission and resolution for toolsrv. +// Package bypass handles bypass request submission and resolution for toolsrvclient. // Bypass requests are submitted by sandboxed execution, exposed via 9P for // external approval (olliesrv), and resolved when the approver responds. package bypass diff --git a/cmd/toolsrv/internal/exec/exec.go b/cmd/toolsrv/internal/exec/exec.go index cd465f7..3980a9d 100644 --- a/cmd/toolsrv/internal/exec/exec.go +++ b/cmd/toolsrv/internal/exec/exec.go @@ -17,7 +17,8 @@ import ( "ollie/cmd/toolsrv/internal/bypass" "ollie/cmd/toolsrv/internal/sandbox" "ollie/paths" - "ollie/toolsrv" + "ollie/toolsrv/metadata" + "ollie/toolsrv/protocol" ) // Config contains all context needed for tool execution. @@ -46,9 +47,9 @@ func WithStreamFunc(ctx context.Context, fn func(string)) context.Context { } // ExecuteTool runs a tool script with the given args and returns the result. -func ExecuteTool(ctx context.Context, info toolsrv.ToolInfo, args json.RawMessage, cfg Config) (json.RawMessage, error) { +func ExecuteTool(ctx context.Context, info protocol.ToolInfo, args json.RawMessage, cfg Config) (json.RawMessage, error) { // Resolve script path - toolPath, err := toolsrv.ResolveTool(info.Name) + toolPath, err := metadata.ResolveTool(info.Name) if err != nil { return nil, fmt.Errorf("resolve tool %s: %w", info.Name, err) } @@ -99,13 +100,13 @@ func ExecuteTool(ctx context.Context, info toolsrv.ToolInfo, args json.RawMessag } if err != nil { - return json.Marshal(toolsrv.ToolResult{ + return json.Marshal(protocol.ToolResult{ IsError: true, - Content: []toolsrv.ToolResultContent{{Type: "text", Text: result + ": " + err.Error()}}, + Content: []protocol.ToolResultContent{{Type: "text", Text: result + ": " + err.Error()}}, }) } - return json.Marshal(toolsrv.ToolResult{ - Content: []toolsrv.ToolResultContent{{Type: "text", Text: result}}, + return json.Marshal(protocol.ToolResult{ + Content: []protocol.ToolResultContent{{Type: "text", Text: result}}, }) } @@ -144,7 +145,7 @@ func executeSandboxed(ctx context.Context, toolPath, stdinData, cwd string, envE envMap[k] = v } // Prepend tools directory to PATH so meta-only cmd fields can reference other tools - if toolsDir := toolsrv.ToolsPath(); toolsDir != "" { + if toolsDir := metadata.ToolsPath(); toolsDir != "" { if existing := envMap["PATH"]; existing != "" { envMap["PATH"] = toolsDir + ":" + existing } else { diff --git a/cmd/toolsrv/internal/registry/registry.go b/cmd/toolsrv/internal/registry/registry.go index 88ef156..7ed0ff1 100644 --- a/cmd/toolsrv/internal/registry/registry.go +++ b/cmd/toolsrv/internal/registry/registry.go @@ -1,4 +1,4 @@ -// Package registry provides the tool registry for toolsrv. +// Package registry provides the tool registry for toolsrvclient. // Tools are scoped per-agent within a session (one toolsrv per session). package registry @@ -7,28 +7,29 @@ import ( "sort" "sync" - "ollie/toolsrv" + "ollie/toolsrv/metadata" + "ollie/toolsrv/protocol" ) // Registry tracks loaded tools per agent. type Registry struct { mu sync.RWMutex - agents map[string]map[string]toolsrv.ToolInfo + agents map[string]map[string]protocol.ToolInfo revisions map[string]uint64 } // New creates a new tool registry. func New() (*Registry, error) { return &Registry{ - agents: make(map[string]map[string]toolsrv.ToolInfo), + agents: make(map[string]map[string]protocol.ToolInfo), revisions: make(map[string]uint64), }, nil } // discover scans the tools directory and returns all valid tool infos. -func (r *Registry) discover() map[string]toolsrv.ToolInfo { - infos := toolsrv.DiscoverTools() - tools := make(map[string]toolsrv.ToolInfo, len(infos)) +func (r *Registry) discover() map[string]protocol.ToolInfo { + infos := metadata.DiscoverTools() + tools := make(map[string]protocol.ToolInfo, len(infos)) for _, info := range infos { tools[info.Name] = info } @@ -47,7 +48,7 @@ func (r *Registry) Load(agentID, name string) error { defer r.mu.Unlock() if r.agents[agentID] == nil { - r.agents[agentID] = make(map[string]toolsrv.ToolInfo) + r.agents[agentID] = make(map[string]protocol.ToolInfo) } r.agents[agentID][name] = tool @@ -75,7 +76,7 @@ func (r *Registry) Unload(agentID, name string) error { } // Loaded returns all loaded tools for an agent. -func (r *Registry) Loaded(agentID string) []toolsrv.ToolInfo { +func (r *Registry) Loaded(agentID string) []protocol.ToolInfo { r.mu.RLock() defer r.mu.RUnlock() @@ -84,7 +85,7 @@ func (r *Registry) Loaded(agentID string) []toolsrv.ToolInfo { return nil } - var loaded []toolsrv.ToolInfo + var loaded []protocol.ToolInfo for _, info := range agentTools { loaded = append(loaded, info) } @@ -93,13 +94,13 @@ func (r *Registry) Loaded(agentID string) []toolsrv.ToolInfo { } // Lookup returns a tool by name for an agent. -func (r *Registry) Lookup(agentID, name string) (toolsrv.ToolInfo, bool) { +func (r *Registry) Lookup(agentID, name string) (protocol.ToolInfo, bool) { r.mu.RLock() defer r.mu.RUnlock() agentTools, ok := r.agents[agentID] if !ok { - return toolsrv.ToolInfo{}, false + return protocol.ToolInfo{}, false } tool, exists := agentTools[name] diff --git a/cmd/toolsrv/internal/server/proc.go b/cmd/toolsrv/internal/server/proc.go index 73b8acf..cb546a5 100644 --- a/cmd/toolsrv/internal/server/proc.go +++ b/cmd/toolsrv/internal/server/proc.go @@ -1,4 +1,4 @@ -// proc.go - Process management and tool operations for toolsrv. +// proc.go - Process management and tool operations for toolsrvclient. package server import ( @@ -19,10 +19,10 @@ import ( "ollie/cmd/toolsrv/internal/exec" "ollie/cmd/toolsrv/internal/registry" "ollie/paths" - "ollie/toolsrv" + "ollie/toolsrv/protocol" ) -// State implements the filesystem state for toolsrv. +// State implements the filesystem state for toolsrvclient. type State struct { mu sync.RWMutex @@ -40,7 +40,7 @@ type State struct { // Process management procMu sync.Mutex procs map[int]*Proc - nextID int + nextID int procLimit int // Callbacks @@ -90,7 +90,7 @@ func NewState(cwd string) *State { cwd: cwd, env: make(map[string]string), procs: make(map[int]*Proc), - nextID: 1, + nextID: 1, procLimit: 32, } } @@ -143,7 +143,7 @@ func (st *State) GetEnv(key string) string { // --- Tool Registry --- // ListTools returns loaded tools for the given agent. -func (st *State) ListTools(agentID string) []toolsrv.ToolInfo { +func (st *State) ListTools(agentID string) []protocol.ToolInfo { if agentID == "" { return nil } @@ -217,7 +217,7 @@ func (st *State) allocID() int { // If background is true, returns immediately with pid. func (st *State) NewProc(ctx context.Context, payload string, background bool) (result string, pid int, err error) { // Parse payload - args := toolsrv.ParsePayload(payload) + args := protocol.ParsePayload(payload) toolName := args["tool"] if toolName == "" { return "", 0, fmt.Errorf("missing 'tool' in payload") @@ -247,10 +247,10 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) ( return "", 0, fmt.Errorf("missing 'agent' in payload") } envCopy["OLLIE_UNAME"] = aid - + // Get session ID from environment sid := envCopy["OLLIE_SESSION_ID"] - + // Get human-readable command cmd := args["cmd"] if cmd == "" { @@ -325,7 +325,7 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) ( proc.Exited = true proc.EndTime = time.Now() proc.mu.Unlock() - + // Notify on background proc exit if background && st.OnProcExit != nil { st.OnProcExit(proc) @@ -378,7 +378,7 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) ( // executeTool runs a tool and returns output + exit code. // timeout: 0 = no timeout, >0 = seconds. -func (st *State) executeTool(ctx context.Context, info toolsrv.ToolInfo, args map[string]string, cwd string, envExtra map[string]string, yolo bool, timeout int, output io.Writer, started chan *os.Process) (string, int) { +func (st *State) executeTool(ctx context.Context, info protocol.ToolInfo, args map[string]string, cwd string, envExtra map[string]string, yolo bool, timeout int, output io.Writer, started chan *os.Process) (string, int) { // Convert args map to JSON for the execution path jsonArgs := argsToJSON(args) diff --git a/cmd/toolsrv/internal/server/proc_test.go b/cmd/toolsrv/internal/server/proc_test.go index a99c3f3..c8ea2d9 100644 --- a/cmd/toolsrv/internal/server/proc_test.go +++ b/cmd/toolsrv/internal/server/proc_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "ollie/toolsrv" + "ollie/toolsrv/protocol" ) func TestState_Basic(t *testing.T) { @@ -134,7 +134,7 @@ func TestState_HandleProcCtl(t *testing.T) { func TestProc_Stat(t *testing.T) { proc := &Proc{ - ID: 1, + ID: 1, Tool: "shell", StartTime: time.Now(), done: make(chan struct{}), @@ -191,10 +191,10 @@ func TestParsePayload(t *testing.T) { } for _, tt := range tests { - got := toolsrv.ParsePayload(tt.input) + got := protocol.ParsePayload(tt.input) for k, v := range tt.want { if got[k] != v { - t.Errorf("toolsrv.ParsePayload(%q)[%q] = %q, want %q", tt.input, k, got[k], v) + t.Errorf("protocol.ParsePayload(%q)[%q] = %q, want %q", tt.input, k, got[k], v) } } } @@ -206,7 +206,7 @@ func TestGcProcs(t *testing.T) { // Add a proc that's exited and was read long ago st.procMu.Lock() st.procs[1] = &Proc{ - ID: 1, + ID: 1, Exited: true, EndTime: time.Now().Add(-20 * time.Minute), LastRead: time.Now().Add(-15 * time.Minute), @@ -214,20 +214,20 @@ func TestGcProcs(t *testing.T) { } // Add a proc that's exited but never read st.procs[2] = &Proc{ - ID: 2, + ID: 2, Exited: true, done: make(chan struct{}), } // Add a proc that's exited and recently read st.procs[3] = &Proc{ - ID: 3, + ID: 3, Exited: true, LastRead: time.Now(), done: make(chan struct{}), } // Add a still-running proc st.procs[4] = &Proc{ - ID: 4, + ID: 4, Exited: false, LastRead: time.Now().Add(-20 * time.Minute), done: make(chan struct{}), diff --git a/cmd/toolsrv/internal/server/server.go b/cmd/toolsrv/internal/server/server.go index a4dadc5..a54e4dc 100644 --- a/cmd/toolsrv/internal/server/server.go +++ b/cmd/toolsrv/internal/server/server.go @@ -1,4 +1,4 @@ -// spec.go - virtfs-based namespace specification for toolsrv. +// spec.go - virtfs-based namespace specification for toolsrvclient. package server import ( @@ -15,7 +15,7 @@ import ( "ollie/cmd/toolsrv/internal/bypass" "ollie/cmd/toolsrv/internal/registry" - "ollie/toolsrv" + "ollie/toolsrv/metadata" "ollie/virtfs" ) @@ -23,10 +23,10 @@ import ( type Server struct { mu sync.RWMutex - secret string // set on first auth, verified on subsequent - token string // session token returned after auth - registry *registry.Registry // tool registry - yolo bool // skip sandbox + secret string // set on first auth, verified on subsequent + token string // session token returned after auth + registry *registry.Registry // tool registry + yolo bool // skip sandbox // Process management Fs *State @@ -129,7 +129,7 @@ func Spec(srv *Server) virtfs.FsNodeDecl { virtfs.FileNode("all", 0444, virtfs.Doc("All available tools on disk: namedescription per line"), virtfs.Read(func() ([]byte, error) { - tools := toolsrv.DiscoverTools() + tools := metadata.DiscoverTools() var sb strings.Builder for _, ti := range tools { fmt.Fprintf(&sb, "%s\t%s\n", ti.Name, ti.Description) diff --git a/cmd/toolsrv/main.go b/cmd/toolsrv/main.go index 003ff35..fb114e3 100644 --- a/cmd/toolsrv/main.go +++ b/cmd/toolsrv/main.go @@ -26,8 +26,8 @@ import ( "syscall" "time" - "ollie/cmd/toolsrv/internal/server" "ollie/cmd/toolsrv/internal/registry" + "ollie/cmd/toolsrv/internal/server" "ollie/env" lib9p "ollie/lib9p/client" @@ -36,11 +36,11 @@ import ( ) var ( - cwd = flag.String("cwd", ".", "working directory for execution") - listenPath = flag.String("listen", "", "Unix socket path to listen on (required)") - sessionID = flag.String("session-id", "", "session ID for tool registry") - yolo = flag.Bool("yolo", false, "skip sandbox enforcement") - noAuth = flag.Bool("no-auth", false, "disable Tauth authentication (for debugging)") + cwd = flag.String("cwd", ".", "working directory for execution") + listenPath = flag.String("listen", "", "Unix socket path to listen on (required)") + sessionID = flag.String("session-id", "", "session ID for tool registry") + yolo = flag.Bool("yolo", false, "skip sandbox enforcement") + noAuth = flag.Bool("no-auth", false, "disable Tauth authentication (for debugging)") idleTimeout = flag.Duration("idle-timeout", 30*time.Second, "exit after this duration with no connections (0 to disable)") ) @@ -89,7 +89,7 @@ func main() { if toolReg != nil { srv.SetRegistry(toolReg) } - + // Set up callback to push completed proc output to agent prompt srv.Fs.OnProcExit = func(proc *server.Proc) { if proc.SessionID == "" || proc.AgentID == "" { @@ -227,10 +227,10 @@ func pushProcInterrupt(proc *server.Proc) { sid := proc.SessionID aid := proc.AgentID proc.Unlock() - + msg := fmt.Sprintf("\n%s\n", pid, escapeAttr(cmd), exitCode, output) - + // Connect to olliesrv u, err := user.Current() if err != nil { @@ -242,21 +242,21 @@ func pushProcInterrupt(proc *server.Proc) { display = ":0" } sockPath := fmt.Sprintf("/tmp/ns.%s.%s/ollie", u.Username, display) - + fsys, err := lib9p.Dial(sockPath, u.Username, "") if err != nil { logger.Warn("pushProcInterrupt: failed to connect to olliesrv: %v", err) return } defer fsys.Close() - + // Write to session/{sid}/agent/{aid}/prompt promptPath := fmt.Sprintf("session/%s/agent/%s/prompt", sid, aid) if err := fsys.Write(promptPath, []byte(msg)); err != nil { logger.Warn("pushProcInterrupt: failed to write prompt: %v", err) return } - + logger.Info("pushed proc %d interrupt to %s", pid, promptPath) } diff --git a/cmd/toolsrv/p9.go b/cmd/toolsrv/p9.go index 2835dae..cf84196 100644 --- a/cmd/toolsrv/p9.go +++ b/cmd/toolsrv/p9.go @@ -1,4 +1,4 @@ -// p9.go - 9P filesystem server for toolsrv. +// p9.go - 9P filesystem server for toolsrvclient. package main import ( diff --git a/toolsrv/client9p.go b/toolsrv/client/client.go similarity index 96% rename from toolsrv/client9p.go rename to toolsrv/client/client.go index fb7ad82..8eb9a3a 100644 --- a/toolsrv/client9p.go +++ b/toolsrv/client/client.go @@ -1,5 +1,5 @@ -// client9p.go - 9P client for toolsrv. -package toolsrv +// client9p.go - 9P client for toolsrvclient. +package client import ( "context" @@ -12,9 +12,11 @@ import ( "9fans.net/go/plan9" p9client "9fans.net/go/plan9/client" + + "ollie/toolsrv/protocol" ) -// Conn is a 9P connection to a toolsrv. +// Conn is a 9P connection to a toolsrvclient. type Conn struct { fsys *p9client.Fsys conn *p9client.Conn @@ -102,7 +104,7 @@ func (c *Conn) Token() string { } // ListTools returns the list of loaded tools for this agent. -func (c *Conn) ListTools() ([]ToolInfo, error) { +func (c *Conn) ListTools() ([]protocol.ToolInfo, error) { fid, err := c.fsys.Open("tools", plan9.ORDWR) if err != nil { return nil, err @@ -129,7 +131,7 @@ func (c *Conn) ListTools() ([]ToolInfo, error) { } } - var tools []ToolInfo + var tools []protocol.ToolInfo if err := json.Unmarshal(result, &tools); err != nil { return nil, fmt.Errorf("parse tools: %w", err) } @@ -245,7 +247,7 @@ func (c *Conn) LoadTool(name string) error { return err } -// ListAllTools reads the full catalog of available tools from toolsrv. +// ListAllTools reads the full catalog of available tools from toolsrvclient. func (c *Conn) ListAllTools() ([]byte, error) { fid, err := c.fsys.Open("all", plan9.OREAD) if err != nil { @@ -475,7 +477,7 @@ func (c *Conn) GetDetachedOutput(pid int) (string, error) { // --- Helper types --- -// HostInfo contains information about the host running toolsrv. +// HostInfo contains information about the host running toolsrvclient. type HostInfo struct { Platform string IsGitRepo bool @@ -539,18 +541,10 @@ func escapeValue(s string) string { return string(result) } -// BypassRequest represents a bypass request from toolsrv. -type BypassRequest struct { - ID string `json:"id"` - Cmd string `json:"cmd"` - Cwd string `json:"cwd"` - Env map[string]string `json:"env,omitempty"` -} - // ReadBypassPending blocks until a bypass request is available. // This should be called in a loop by the approval handler. // Returns nil, ctx.Err() if context is cancelled. -func (c *Conn) ReadBypassPending(ctx context.Context) (*BypassRequest, error) { +func (c *Conn) ReadBypassPending(ctx context.Context) (*protocol.BypassRequest, error) { fid, err := c.fsys.Open("bypass/pending", plan9.OREAD) if err != nil { return nil, fmt.Errorf("open bypass/pending: %w", err) @@ -588,7 +582,7 @@ func (c *Conn) ReadBypassPending(ctx context.Context) (*BypassRequest, error) { } fid.Close() - var req BypassRequest + var req protocol.BypassRequest if err := json.Unmarshal(result, &req); err != nil { return nil, fmt.Errorf("parse bypass request: %w", err) } diff --git a/toolsrv/metadata/discover.go b/toolsrv/metadata/discover.go new file mode 100644 index 0000000..64aecd7 --- /dev/null +++ b/toolsrv/metadata/discover.go @@ -0,0 +1,48 @@ +// registry.go - Tool discovery and shared types. +package metadata + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + + "ollie/paths" + "ollie/toolsrv/protocol" +) + +// ToolsPath returns ~/.config/ollie/tools. +func ToolsPath() string { + return paths.CfgDir() + "/tools" +} + +// DiscoverTools scans the tools directory for .meta files and returns +// metadata for all tools. +func DiscoverTools() []protocol.ToolInfo { + dir := ToolsPath() + entries, err := os.ReadDir(dir) + if err != nil { + return nil + } + var infos []protocol.ToolInfo + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".meta") { + continue + } + name := strings.TrimSuffix(e.Name(), ".meta") + data, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + continue + } + var m MetaFile + if err := json.Unmarshal(data, &m); err != nil { + continue + } + info := ToolInfoFromMeta(name, &m) + if info.Name == "" { + continue // no variant matched — tool unavailable on this host + } + infos = append(infos, info) + } + return infos +} diff --git a/toolsrv/meta.go b/toolsrv/metadata/meta.go similarity index 97% rename from toolsrv/meta.go rename to toolsrv/metadata/meta.go index e4a2735..a9c8f3c 100644 --- a/toolsrv/meta.go +++ b/toolsrv/metadata/meta.go @@ -1,8 +1,9 @@ -package toolsrv +package metadata import ( "encoding/json" "fmt" + "ollie/toolsrv/protocol" "os" "os/exec" "path/filepath" @@ -162,10 +163,10 @@ func checkCondition(key, val string) bool { // ToolInfoFromMeta builds a ToolInfo from a MetaFile. // Applies variant resolution first. Returns zero ToolInfo if no variant matches. -func ToolInfoFromMeta(name string, m *MetaFile) ToolInfo { +func ToolInfoFromMeta(name string, m *MetaFile) protocol.ToolInfo { resolved := m.Resolve() if resolved == nil { - return ToolInfo{} + return protocol.ToolInfo{} } args := resolved.Args if args == nil { @@ -175,7 +176,7 @@ func ToolInfoFromMeta(name string, m *MetaFile) ToolInfo { if tier == "" { tier = "hot" } - return ToolInfo{ + return protocol.ToolInfo{ Name: name, Description: resolved.Description, InputSchema: args, diff --git a/toolsrv/parse.go b/toolsrv/protocol/payload.go similarity index 98% rename from toolsrv/parse.go rename to toolsrv/protocol/payload.go index a508336..1d5d0e7 100644 --- a/toolsrv/parse.go +++ b/toolsrv/protocol/payload.go @@ -1,5 +1,5 @@ // parse.go — Shared key=value payload parsing. -package toolsrv +package protocol import "strings" diff --git a/toolsrv/protocol/types.go b/toolsrv/protocol/types.go new file mode 100644 index 0000000..ab60247 --- /dev/null +++ b/toolsrv/protocol/types.go @@ -0,0 +1,39 @@ +// types.go - Shared types for toolsrv +package protocol + +import "encoding/json" + +// ToolInfo describes a tool provided by a server. +type ToolInfo struct { + Server string + Name string + Description string + InputSchema json.RawMessage + Prompt string + Tier string + Scope string + OutputFormat string + ResetsCounter bool +} + +// ToolResult is the structured result from a tool execution. +type ToolResult struct { + Content []ToolResultContent `json:"content"` + IsError bool `json:"isError,omitempty"` +} + +// ToolResultContent is a single content item in a tool result. +type ToolResultContent struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + MediaType string `json:"mediaType,omitempty"` + Data string `json:"data,omitempty"` +} + +// BypassRequest represents a bypass request from toolsrv. +type BypassRequest struct { + ID string `json:"id"` + Cmd string `json:"cmd"` + Cwd string `json:"cwd"` + Env map[string]string `json:"env,omitempty"` +} diff --git a/toolsrv/registry.go b/toolsrv/registry.go deleted file mode 100644 index 7cf0eed..0000000 --- a/toolsrv/registry.go +++ /dev/null @@ -1,74 +0,0 @@ -// registry.go - Tool discovery and shared types. -package toolsrv - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - - "ollie/paths" -) - -// ToolInfo describes a tool provided by a server. -type ToolInfo struct { - Server string - Name string - Description string - InputSchema json.RawMessage - // Prompt is the usage documentation for this tool, extracted from - // the script's ollie:prompt block. Included in the system prompt. - Prompt string - // Tier is the retention tier: "hot", "warm", or "cold". Parsed from - // the script's ollie:tier annotation. Empty defaults to "hot". - Tier string - // Scope classifies the tool's concurrency behavior for parallel dispatch: - // "read" — path-scoped read; never conflicts (always parallelizable) - // "write" — path-scoped write; conflicts only on same file path - // "global" — full serialization barrier; runs alone - // Empty defaults to inference: "write" if tool has a path arg, else "global". - Scope string - // OutputFormat is the source-fence language for tool output. Empty means plaintext. - OutputFormat string - // ResetsCounter is true for action-oriented tools (file_write, lsp_rename, etc.) - // that indicate active progress rather than passive research. When true, the - // agent loop resets its step counter, allowing continued work without hitting - // the soft step-budget guardrail. - ResetsCounter bool -} - -// ToolsPath returns ~/.config/ollie/tools. -func ToolsPath() string { - return paths.CfgDir() + "/tools" -} - -// DiscoverTools scans the tools directory for .meta files and returns -// metadata for all tools. -func DiscoverTools() []ToolInfo { - dir := ToolsPath() - entries, err := os.ReadDir(dir) - if err != nil { - return nil - } - var infos []ToolInfo - for _, e := range entries { - if e.IsDir() || !strings.HasSuffix(e.Name(), ".meta") { - continue - } - name := strings.TrimSuffix(e.Name(), ".meta") - data, err := os.ReadFile(filepath.Join(dir, e.Name())) - if err != nil { - continue - } - var m MetaFile - if err := json.Unmarshal(data, &m); err != nil { - continue - } - info := ToolInfoFromMeta(name, &m) - if info.Name == "" { - continue // no variant matched — tool unavailable on this host - } - infos = append(infos, info) - } - return infos -} diff --git a/toolsrv/types.go b/toolsrv/types.go deleted file mode 100644 index fddf7fa..0000000 --- a/toolsrv/types.go +++ /dev/null @@ -1,16 +0,0 @@ -// types.go - Shared types for toolsrv -package toolsrv - -// ToolResult is the structured result from a tool execution. -type ToolResult struct { - Content []ToolResultContent `json:"content"` - IsError bool `json:"isError,omitempty"` -} - -// ToolResultContent is a single content item in a tool result. -type ToolResultContent struct { - Type string `json:"type"` - Text string `json:"text,omitempty"` - MediaType string `json:"mediaType,omitempty"` - Data string `json:"data,omitempty"` -}