refactor toolsrv into client protocol and metadata packages
This commit is contained in:
parent
e972b5666a
commit
81933e5281
|
|
@ -27,8 +27,9 @@ import (
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"9fans.net/go/plan9"
|
|
||||||
p9client "ollie/lib9p/client"
|
p9client "ollie/lib9p/client"
|
||||||
|
|
||||||
|
"9fans.net/go/plan9"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
toolsrvclient "ollie/toolsrv/client"
|
||||||
|
"ollie/toolsrv/protocol"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"ollie/toolsrv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// bypassNotifier handles desktop notifications for bypass requests.
|
// 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 {
|
type bypassNotifier struct {
|
||||||
conn *dbus.Conn
|
conn *dbus.Conn
|
||||||
|
|
||||||
|
|
@ -23,7 +25,7 @@ type pendingRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type sessionRef struct {
|
type sessionRef struct {
|
||||||
dialFn func() *toolsrv.Conn
|
dialFn func() *toolsrvclient.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
var notifier *bypassNotifier
|
var notifier *bypassNotifier
|
||||||
|
|
@ -50,15 +52,15 @@ func initBypassNotifier(conn *dbus.Conn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// notifyBypass sends a desktop notification for a bypass request.
|
// notifyBypass sends a desktop notification for a bypass request.
|
||||||
// When the user responds, it writes the resolution directly to toolsrv.
|
// When the user responds, it writes the resolution directly to toolsrvclient.
|
||||||
func notifyBypass(req *toolsrv.BypassRequest, sessionID string, dialFn func() *toolsrv.Conn) {
|
func notifyBypass(req *protocol.BypassRequest, sessionID string, dialFn func() *toolsrvclient.Conn) {
|
||||||
if notifier == nil {
|
if notifier == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
notifier.sendNotification(req, sessionID, dialFn)
|
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")
|
obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
|
||||||
|
|
||||||
summary := "Elevation Request"
|
summary := "Elevation Request"
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import (
|
||||||
lib9p "ollie/lib9p/client"
|
lib9p "ollie/lib9p/client"
|
||||||
olog "ollie/log"
|
olog "ollie/log"
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
"ollie/toolsrv"
|
toolsrvclient "ollie/toolsrv/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Agent holds the state of the current Agent entity (the "agent"
|
// Agent holds the state of the current Agent entity (the "agent"
|
||||||
|
|
@ -28,7 +28,7 @@ type Agent struct {
|
||||||
agentsDir string
|
agentsDir string
|
||||||
systemPrompt string // system prompt for /agent reloads
|
systemPrompt string // system prompt for /agent reloads
|
||||||
envBlock string // environment block for /agent reloads
|
envBlock string // environment block for /agent reloads
|
||||||
newToolServer func() *toolsrv.Conn
|
newToolServer func() *toolsrvclient.Conn
|
||||||
newBackend func(string) (backend.Backend, error)
|
newBackend func(string) (backend.Backend, error)
|
||||||
currentAction atomic.Pointer[actionHandle]
|
currentAction atomic.Pointer[actionHandle]
|
||||||
warnedContext bool
|
warnedContext bool
|
||||||
|
|
@ -331,7 +331,7 @@ func (ag *Agent) emit(ev Event) {
|
||||||
|
|
||||||
// SetToolServer updates the tool server connection and factory.
|
// SetToolServer updates the tool server connection and factory.
|
||||||
// Used when resuming a paused session that was restored without infra.
|
// 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
|
ag.newToolServer = newToolServer
|
||||||
if ag.runtime != nil {
|
if ag.runtime != nil {
|
||||||
ag.runtime.ToolServer = conn
|
ag.runtime.ToolServer = conn
|
||||||
|
|
@ -623,7 +623,7 @@ func (ag *Agent) ListModels() []string {
|
||||||
|
|
||||||
// ToolServer returns the tool execution server, or nil if unavailable.
|
// ToolServer returns the tool execution server, or nil if unavailable.
|
||||||
// Exported for use by the 9P filesystem layer to sync tool registries.
|
// 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
|
return ag.runtime.ToolServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -688,7 +688,7 @@ type AgentParams struct {
|
||||||
Cwd string // working directory for tool execution
|
Cwd string // working directory for tool execution
|
||||||
SystemPrompt string
|
SystemPrompt string
|
||||||
EnvBlock string
|
EnvBlock string
|
||||||
NewToolServer func() *toolsrv.Conn
|
NewToolServer func() *toolsrvclient.Conn
|
||||||
NewBackend func(string) (backend.Backend, error)
|
NewBackend func(string) (backend.Backend, error)
|
||||||
Log *olog.Logger
|
Log *olog.Logger
|
||||||
StartupMsgs []string
|
StartupMsgs []string
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRefreshToolListing(t *testing.T) {
|
func TestRefreshToolListing(t *testing.T) {
|
||||||
infos := []toolsrv.ToolInfo{
|
infos := []protocol.ToolInfo{
|
||||||
{
|
{
|
||||||
Name: "new_tool",
|
Name: "new_tool",
|
||||||
Description: "New description",
|
Description: "New description",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
"ollie/cmd/olliesrv/internal/backend"
|
"ollie/cmd/olliesrv/internal/backend"
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
"ollie/toolsrv"
|
toolsrvclient "ollie/toolsrv/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxTransientRetries = 3
|
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)})
|
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
|
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) {
|
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)
|
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 {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"ollie/cmd/olliesrv/internal/backend"
|
"ollie/cmd/olliesrv/internal/backend"
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestToolConflictKeys(t *testing.T) {
|
func TestToolConflictKeys(t *testing.T) {
|
||||||
rt := &Runtime{
|
rt := &Runtime{
|
||||||
ToolMeta: map[string]toolsrv.ToolInfo{
|
ToolMeta: map[string]protocol.ToolInfo{
|
||||||
"file_read": {Name: "file_read", Scope: "read"},
|
"file_read": {Name: "file_read", Scope: "read"},
|
||||||
"file_grep": {Name: "file_grep", Scope: "read"},
|
"file_grep": {Name: "file_grep", Scope: "read"},
|
||||||
"file_edit": {Name: "file_edit", Scope: "write"},
|
"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.
|
// Verify the batching logic groups correctly without actually executing.
|
||||||
// We test by inspecting which calls would be grouped together.
|
// We test by inspecting which calls would be grouped together.
|
||||||
rt := &Runtime{
|
rt := &Runtime{
|
||||||
ToolMeta: map[string]toolsrv.ToolInfo{
|
ToolMeta: map[string]protocol.ToolInfo{
|
||||||
"file_read": {Name: "file_read", Scope: "read"},
|
"file_read": {Name: "file_read", Scope: "read"},
|
||||||
"file_edit": {Name: "file_edit", Scope: "write"},
|
"file_edit": {Name: "file_edit", Scope: "write"},
|
||||||
"file_write": {Name: "file_write", Scope: "write"},
|
"file_write": {Name: "file_write", Scope: "write"},
|
||||||
|
|
@ -144,7 +144,7 @@ func TestExecToolCallsBatching(t *testing.T) {
|
||||||
|
|
||||||
func TestExecToolCallsSamePathSerializes(t *testing.T) {
|
func TestExecToolCallsSamePathSerializes(t *testing.T) {
|
||||||
rt := &Runtime{
|
rt := &Runtime{
|
||||||
ToolMeta: map[string]toolsrv.ToolInfo{
|
ToolMeta: map[string]protocol.ToolInfo{
|
||||||
"file_edit": {Name: "file_edit", Scope: "write"},
|
"file_edit": {Name: "file_edit", Scope: "write"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ import (
|
||||||
|
|
||||||
"ollie/cmd/olliesrv/internal/backend"
|
"ollie/cmd/olliesrv/internal/backend"
|
||||||
"ollie/cmd/olliesrv/internal/prompts"
|
"ollie/cmd/olliesrv/internal/prompts"
|
||||||
"ollie/toolsrv"
|
toolsrvclient "ollie/toolsrv/client"
|
||||||
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Preamble sections ---
|
// --- 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
|
// renderTools produces a single combined section: a one-line summary per tool
|
||||||
// followed immediately by its documentation (if any). This keeps the listing
|
// followed immediately by its documentation (if any). This keeps the listing
|
||||||
// and usage instructions adjacent so models don't lose context between them.
|
// and usage instructions adjacent so models don't lose context between them.
|
||||||
func RenderTools(infos []toolsrv.ToolInfo) string {
|
func RenderTools(infos []protocol.ToolInfo) string {
|
||||||
infos = append([]toolsrv.ToolInfo(nil), infos...)
|
infos = append([]protocol.ToolInfo(nil), infos...)
|
||||||
sort.SliceStable(infos, func(i, j int) bool {
|
sort.SliceStable(infos, func(i, j int) bool {
|
||||||
if infos[i].Name == infos[j].Name {
|
if infos[i].Name == infos[j].Name {
|
||||||
return infos[i].Server < infos[j].Server
|
return infos[i].Server < infos[j].Server
|
||||||
|
|
@ -138,10 +139,10 @@ func RenderTools(infos []toolsrv.ToolInfo) string {
|
||||||
// agents replaces it atomically.
|
// agents replaces it atomically.
|
||||||
type Runtime struct {
|
type Runtime struct {
|
||||||
Backend backend.Backend
|
Backend backend.Backend
|
||||||
ToolServer *toolsrv.Conn
|
ToolServer *toolsrvclient.Conn
|
||||||
Preamble *Preamble
|
Preamble *Preamble
|
||||||
Tools []backend.Tool
|
Tools []backend.Tool
|
||||||
ToolMeta map[string]toolsrv.ToolInfo
|
ToolMeta map[string]protocol.ToolInfo
|
||||||
Exec toolExecutor
|
Exec toolExecutor
|
||||||
GenParams backend.GenerationParams
|
GenParams backend.GenerationParams
|
||||||
MaxSteps int
|
MaxSteps int
|
||||||
|
|
@ -160,10 +161,10 @@ func (rt *Runtime) PreambleString() string {
|
||||||
|
|
||||||
// BuildRuntime constructs a Runtime from a pre-configured tool server and
|
// BuildRuntime constructs a Runtime from a pre-configured tool server and
|
||||||
// optional agent config.
|
// 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 messages []string
|
||||||
|
|
||||||
var allToolInfos []toolsrv.ToolInfo
|
var allToolInfos []protocol.ToolInfo
|
||||||
var allTools []backend.Tool
|
var allTools []backend.Tool
|
||||||
|
|
||||||
if srv != nil && (cfg == nil || cfg.ToolsEnabled()) {
|
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
|
return text, blocks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
toolMeta := make(map[string]toolsrv.ToolInfo, len(allToolInfos))
|
toolMeta := make(map[string]protocol.ToolInfo, len(allToolInfos))
|
||||||
for _, ti := range allToolInfos {
|
for _, ti := range allToolInfos {
|
||||||
toolMeta[ti.Name] = ti
|
toolMeta[ti.Name] = ti
|
||||||
}
|
}
|
||||||
|
|
@ -250,7 +251,7 @@ func ResolveSystemPrompt(cfg *AgentConfig) string {
|
||||||
return prompts.DefaultSystemPrompt
|
return prompts.DefaultSystemPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
func toolInfosToBackend(infos []toolsrv.ToolInfo) []backend.Tool {
|
func toolInfosToBackend(infos []protocol.ToolInfo) []backend.Tool {
|
||||||
out := make([]backend.Tool, len(infos))
|
out := make([]backend.Tool, len(infos))
|
||||||
for i, t := range infos {
|
for i, t := range infos {
|
||||||
out[i] = backend.Tool{
|
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
|
// extractToolResult parses the tool server's JSON-RPC response into text
|
||||||
// and optional content blocks.
|
// 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) {
|
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 {
|
if err := json.Unmarshal(raw, &result); err != nil {
|
||||||
return string(raw), nil, false
|
return string(raw), nil, false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEnvironmentBlockStable(t *testing.T) {
|
func TestEnvironmentBlockStable(t *testing.T) {
|
||||||
|
|
@ -19,7 +19,7 @@ func TestEnvironmentBlockStable(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPreambleSections(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_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: "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(`{}`)},
|
{Name: "shell", Description: "Execute shell command", InputSchema: []byte(`{}`)},
|
||||||
|
|
@ -41,9 +41,9 @@ func TestPreambleSections(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderToolsStableOrder(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)
|
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") {
|
if first != second || strings.Index(first, "a_tool") > strings.Index(first, "z_tool") {
|
||||||
t.Fatalf("tool rendering is not deterministic: %q", first)
|
t.Fatalf("tool rendering is not deterministic: %q", first)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
"ollie/cmd/olliesrv/internal/backend"
|
"ollie/cmd/olliesrv/internal/backend"
|
||||||
"ollie/cmd/olliesrv/internal/metrics"
|
"ollie/cmd/olliesrv/internal/metrics"
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Submit processes one line of user input: it starts an agent turn that streams
|
// 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 ""
|
return ""
|
||||||
}
|
}
|
||||||
ag.runtime.Tools = toolInfosToBackend(ti)
|
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 {
|
for _, info := range ti {
|
||||||
meta[info.Name] = info
|
meta[info.Name] = info
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,3 @@ func NewGemini(apiKey string) (*OpenAIBackend, error) {
|
||||||
b.model = geminiDefaultModel
|
b.model = geminiDefaultModel
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1078,7 +1078,6 @@ func kiroRandomJitter() time.Duration {
|
||||||
return time.Duration(n%uint32(kiroTokenExpiryJitter/time.Millisecond)) * time.Millisecond
|
return time.Duration(n%uint32(kiroTokenExpiryJitter/time.Millisecond)) * time.Millisecond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── SQLite helpers ────────────────────────────────────────────────────────────
|
// ── SQLite helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Kiro supports two auth flows whose tokens live under different SQLite keys:
|
// 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 {
|
func kiroDefaultSQLiteKey() string {
|
||||||
return "sqlite://" + filepath.Join(paths.UserDataHome(), "kiro-cli", "data.sqlite3")
|
return "sqlite://" + filepath.Join(paths.UserDataHome(), "kiro-cli", "data.sqlite3")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -544,4 +544,3 @@ func (b *OpenAIBackend) ChatStream(ctx context.Context, messages []Message, tool
|
||||||
|
|
||||||
return streamRequest(b.client, httpReq, "openai", streamOpenAISSE)
|
return streamRequest(b.client, httpReq, "openai", streamOpenAISSE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -318,7 +318,7 @@ func TestOpenRouterClaude_LastToolCacheControl(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOpenRouterNonClaude_NoCacheControl verifies that a non-Claude model on
|
// 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) {
|
func TestOpenRouterNonClaude_NoCacheControl(t *testing.T) {
|
||||||
top := captureRawRequest(t, "openrouter", "mistral-7b",
|
top := captureRawRequest(t, "openrouter", "mistral-7b",
|
||||||
[]Message{{Role: "system", Content: "be helpful"}, {Role: "user", Content: "hi"}},
|
[]Message{{Role: "system", Content: "be helpful"}, {Role: "user", Content: "hi"}},
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Broker manages bypass request evaluation and policy.
|
// 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 {
|
type Broker struct {
|
||||||
policy *PolicyStore
|
policy *PolicyStore
|
||||||
notify NotifyFunc
|
notify NotifyFunc
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ package fs
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"ollie/virtfs"
|
|
||||||
olog "ollie/log"
|
|
||||||
"ollie/cmd/olliesrv/internal/session"
|
"ollie/cmd/olliesrv/internal/session"
|
||||||
|
olog "ollie/log"
|
||||||
|
"ollie/virtfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config carries all runtime dependencies for the filesystem tree.
|
// Config carries all runtime dependencies for the filesystem tree.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
"ollie/cmd/olliesrv/internal/agent"
|
"ollie/cmd/olliesrv/internal/agent"
|
||||||
"ollie/cmd/olliesrv/internal/session"
|
"ollie/cmd/olliesrv/internal/session"
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv/protocol"
|
||||||
"ollie/virtfs"
|
"ollie/virtfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ func parseAgentNewRequest(data []byte) (session.AgentParams, string, string) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if k, v, ok := strings.Cut(tok, "="); ok {
|
if k, v, ok := strings.Cut(tok, "="); ok {
|
||||||
v = toolsrv.UnescapeValue(v)
|
v = protocol.UnescapeValue(v)
|
||||||
switch k {
|
switch k {
|
||||||
case "cwd":
|
case "cwd":
|
||||||
p.CWD = v
|
p.CWD = v
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ import (
|
||||||
"ollie/cmd/olliesrv/internal/toolclient"
|
"ollie/cmd/olliesrv/internal/toolclient"
|
||||||
olog "ollie/log"
|
olog "ollie/log"
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
"ollie/toolsrv"
|
toolsrvclient "ollie/toolsrv/client"
|
||||||
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Session owns all session-level state: identity, agents, tool server lifecycle.
|
// Session owns all session-level state: identity, agents, tool server lifecycle.
|
||||||
|
|
@ -39,7 +40,7 @@ type Session struct {
|
||||||
// Mutable state protected by mu
|
// Mutable state protected by mu
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
name string // friendly name
|
name string // friendly name
|
||||||
toolsConn *toolsrv.Conn // multiplexed RPC connection
|
toolsConn *toolsrvclient.Conn // multiplexed RPC connection
|
||||||
paused bool
|
paused bool
|
||||||
|
|
||||||
// Autosave
|
// Autosave
|
||||||
|
|
@ -84,7 +85,7 @@ func (s *Session) SetName(name string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolsConn returns the tool server connection.
|
// ToolsConn returns the tool server connection.
|
||||||
func (s *Session) ToolsConn() *toolsrv.Conn {
|
func (s *Session) ToolsConn() *toolsrvclient.Conn {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
return s.toolsConn
|
return s.toolsConn
|
||||||
|
|
@ -92,7 +93,7 @@ func (s *Session) ToolsConn() *toolsrv.Conn {
|
||||||
|
|
||||||
// DialToolServer dials a fresh toolsrv connection via the keeper.
|
// DialToolServer dials a fresh toolsrv connection via the keeper.
|
||||||
// Caller is responsible for closing the returned conn.
|
// Caller is responsible for closing the returned conn.
|
||||||
func (s *Session) DialToolServer() *toolsrv.Conn {
|
func (s *Session) DialToolServer() *toolsrvclient.Conn {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
k := s.Keeper
|
k := s.Keeper
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
|
|
@ -107,7 +108,7 @@ func (s *Session) DialToolServer() *toolsrv.Conn {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetToolsConn sets the tool server connection.
|
// SetToolsConn sets the tool server connection.
|
||||||
func (s *Session) SetToolsConn(conn *toolsrv.Conn) {
|
func (s *Session) SetToolsConn(conn *toolsrvclient.Conn) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
s.toolsConn = conn
|
s.toolsConn = conn
|
||||||
|
|
@ -115,7 +116,7 @@ func (s *Session) SetToolsConn(conn *toolsrv.Conn) {
|
||||||
|
|
||||||
// BypassNotifyFunc is called when a bypass request arrives.
|
// BypassNotifyFunc is called when a bypass request arrives.
|
||||||
// It receives the request, session ID, and a dial function to get a connection for resolution.
|
// 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
|
// StartBypassLoop starts a goroutine that reads bypass requests from toolsrv
|
||||||
// and calls notifyFn for each one. This should be called after SetToolsConn.
|
// 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
|
// Notify (non-blocking) - the notifier will write resolution when user responds
|
||||||
if notifyFn != nil {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("resume agent dial: %w", err)
|
return fmt.Errorf("resume agent dial: %w", err)
|
||||||
}
|
}
|
||||||
ag.SetToolServer(func() *toolsrv.Conn {
|
ag.SetToolServer(func() *toolsrvclient.Conn {
|
||||||
c, _ := s.Keeper.Dial()
|
c, _ := s.Keeper.Dial()
|
||||||
return c
|
return c
|
||||||
}, agConn)
|
}, agConn)
|
||||||
|
|
@ -392,7 +393,7 @@ func (s *Session) LoadTool(name string, ag *agent.Agent) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadToolOnConn loads a tool on the given connection.
|
// 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 {
|
if conn == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"ollie/cmd/olliesrv/internal/agent"
|
"ollie/cmd/olliesrv/internal/agent"
|
||||||
|
"ollie/cmd/olliesrv/internal/prompts"
|
||||||
"ollie/cmd/olliesrv/internal/toolclient"
|
"ollie/cmd/olliesrv/internal/toolclient"
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
"ollie/cmd/olliesrv/internal/prompts"
|
toolsrvclient "ollie/toolsrv/client"
|
||||||
"ollie/toolsrv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SessionInfra holds the tool server infrastructure for a session.
|
// SessionInfra holds the tool server infrastructure for a session.
|
||||||
|
|
@ -16,8 +16,8 @@ import (
|
||||||
type SessionInfra struct {
|
type SessionInfra struct {
|
||||||
Proc *toolclient.Process
|
Proc *toolclient.Process
|
||||||
Keeper *toolclient.ProcessKeeper
|
Keeper *toolclient.ProcessKeeper
|
||||||
ToolsConn *toolsrv.Conn
|
ToolsConn *toolsrvclient.Conn
|
||||||
NewToolServer func() *toolsrv.Conn
|
NewToolServer func() *toolsrvclient.Conn
|
||||||
Platform string
|
Platform string
|
||||||
IsGitRepo bool
|
IsGitRepo bool
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ type ToolServerConfig struct {
|
||||||
type InfraConfig struct {
|
type InfraConfig struct {
|
||||||
Proc *toolclient.Process
|
Proc *toolclient.Process
|
||||||
Keeper *toolclient.ProcessKeeper
|
Keeper *toolclient.ProcessKeeper
|
||||||
ToolsConn *toolsrv.Conn
|
ToolsConn *toolsrvclient.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupToolServer spawns the tool server process and creates the infrastructure.
|
// 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.
|
// Reuse existing infrastructure if provided.
|
||||||
if cfg.ReuseFrom != nil {
|
if cfg.ReuseFrom != nil {
|
||||||
r := cfg.ReuseFrom
|
r := cfg.ReuseFrom
|
||||||
var newToolServer func() *toolsrv.Conn
|
var newToolServer func() *toolsrvclient.Conn
|
||||||
if r.Keeper != nil {
|
if r.Keeper != nil {
|
||||||
newToolServer = func() *toolsrv.Conn {
|
newToolServer = func() *toolsrvclient.Conn {
|
||||||
conn, err := r.Keeper.Dial()
|
conn, err := r.Keeper.Dial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -60,7 +60,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
|
||||||
return conn
|
return conn
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newToolServer = func() *toolsrv.Conn {
|
newToolServer = func() *toolsrvclient.Conn {
|
||||||
return r.ToolsConn
|
return r.ToolsConn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +79,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
|
||||||
}
|
}
|
||||||
var proc *toolclient.Process
|
var proc *toolclient.Process
|
||||||
var keeper *toolclient.ProcessKeeper
|
var keeper *toolclient.ProcessKeeper
|
||||||
var newToolServer func() *toolsrv.Conn
|
var newToolServer func() *toolsrvclient.Conn
|
||||||
var err error
|
var err error
|
||||||
var platform string
|
var platform string
|
||||||
var isGitRepo bool
|
var isGitRepo bool
|
||||||
|
|
@ -102,7 +102,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
|
||||||
Yolo: cfg.Yolo,
|
Yolo: cfg.Yolo,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
newToolServer = func() *toolsrv.Conn {
|
newToolServer = func() *toolsrvclient.Conn {
|
||||||
conn, err := keeper.Dial()
|
conn, err := keeper.Dial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 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) {
|
keeper = toolclient.NewProcessKeeper(cfg.Ctx, proc, func(ctx context.Context) (*toolclient.Process, error) {
|
||||||
return toolclient.Spawn(ctx, cfg.CWD, dialOpts...)
|
return toolclient.Spawn(ctx, cfg.CWD, dialOpts...)
|
||||||
})
|
})
|
||||||
newToolServer = func() *toolsrv.Conn {
|
newToolServer = func() *toolsrvclient.Conn {
|
||||||
conn, err := keeper.Dial()
|
conn, err := keeper.Dial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 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.
|
// 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
|
// 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.
|
// 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 {
|
if cfg == nil || conn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// spawn.go - Process spawning and lifecycle management for toolsrv.
|
// spawn.go - Process spawning and lifecycle management for toolsrvclient.
|
||||||
package toolclient
|
package toolclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -18,7 +18,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
toolsrvclient "ollie/toolsrv"
|
toolsrvclient "ollie/toolsrv/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Process represents a running toolsrv process.
|
// Process represents a running toolsrv process.
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,14 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"9fans.net/go/plan9/client"
|
|
||||||
"ollie/cmd/olliesrv/internal/agent"
|
"ollie/cmd/olliesrv/internal/agent"
|
||||||
"ollie/env"
|
|
||||||
"ollie/cmd/olliesrv/internal/fs"
|
"ollie/cmd/olliesrv/internal/fs"
|
||||||
|
"ollie/env"
|
||||||
olog "ollie/log"
|
olog "ollie/log"
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
|
|
||||||
|
"9fans.net/go/plan9/client"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/cmd/olliesrv/internal/fs"
|
"ollie/cmd/olliesrv/internal/fs"
|
||||||
olog "ollie/log"
|
|
||||||
"ollie/cmd/olliesrv/internal/session"
|
"ollie/cmd/olliesrv/internal/session"
|
||||||
|
olog "ollie/log"
|
||||||
|
|
||||||
"9fans.net/go/plan9"
|
"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"}
|
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.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.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.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.Twalk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
|
||||||
plan9.Topen: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return open(cfg.RootTree, s.groups, cs, fc) },
|
return walk(cfg.RootTree, 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.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.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.Tstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
|
||||||
plan9.Twstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return wstat(cfg.RootTree, cs, fc) },
|
return stat(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.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
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/toolsrv"
|
toolsrvclient "ollie/toolsrv/client"
|
||||||
|
"ollie/toolsrv/metadata"
|
||||||
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testToolsrvBinary string
|
var testToolsrvBinary string
|
||||||
|
|
@ -85,7 +87,7 @@ network:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Symlink the real tools directory so tests can find them
|
// 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.Stat(realToolsPath); err == nil {
|
||||||
if err := os.Symlink(realToolsPath, filepath.Join(cfgDir, "tools")); err != nil {
|
if err := os.Symlink(realToolsPath, filepath.Join(cfgDir, "tools")); err != nil {
|
||||||
os.RemoveAll(tmpDir)
|
os.RemoveAll(tmpDir)
|
||||||
|
|
@ -93,7 +95,7 @@ network:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socketPath = filepath.Join(tmpDir, "toolsrv.sock")
|
socketPath = filepath.Join(tmpDir, "toolsrvclient.sock")
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
@ -140,7 +142,7 @@ func TestIntegration_FirstConnectionSetsSecret(t *testing.T) {
|
||||||
|
|
||||||
// First connection with a secret should succeed
|
// First connection with a secret should succeed
|
||||||
secret := "test-secret-first-connection"
|
secret := "test-secret-first-connection"
|
||||||
conn, err := toolsrv.Dial(socketPath, secret)
|
conn, err := toolsrvclient.Dial(socketPath, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("first Dial failed: %v", err)
|
t.Fatalf("first Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +166,7 @@ func TestIntegration_SameSecretReconnects(t *testing.T) {
|
||||||
secret := "test-secret-reconnect"
|
secret := "test-secret-reconnect"
|
||||||
|
|
||||||
// First connection establishes the secret
|
// First connection establishes the secret
|
||||||
conn1, err := toolsrv.Dial(socketPath, secret)
|
conn1, err := toolsrvclient.Dial(socketPath, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("first Dial failed: %v", err)
|
t.Fatalf("first Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +174,7 @@ func TestIntegration_SameSecretReconnects(t *testing.T) {
|
||||||
conn1.Close()
|
conn1.Close()
|
||||||
|
|
||||||
// Second connection with same secret should succeed
|
// Second connection with same secret should succeed
|
||||||
conn2, err := toolsrv.Dial(socketPath, secret)
|
conn2, err := toolsrvclient.Dial(socketPath, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("second Dial failed: %v", err)
|
t.Fatalf("second Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -189,14 +191,14 @@ func TestIntegration_WrongSecretFails(t *testing.T) {
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// First connection establishes the secret
|
// First connection establishes the secret
|
||||||
conn1, err := toolsrv.Dial(socketPath, "correct-secret")
|
conn1, err := toolsrvclient.Dial(socketPath, "correct-secret")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("first Dial failed: %v", err)
|
t.Fatalf("first Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
conn1.Close()
|
conn1.Close()
|
||||||
|
|
||||||
// Second connection with wrong secret should fail
|
// Second connection with wrong secret should fail
|
||||||
conn2, err := toolsrv.Dial(socketPath, "wrong-secret")
|
conn2, err := toolsrvclient.Dial(socketPath, "wrong-secret")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
conn2.Close()
|
conn2.Close()
|
||||||
t.Fatal("Dial with wrong secret should fail")
|
t.Fatal("Dial with wrong secret should fail")
|
||||||
|
|
@ -208,7 +210,7 @@ func TestIntegration_EmptySecretGeneratesOne(t *testing.T) {
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// Empty secret should generate a random one
|
// Empty secret should generate a random one
|
||||||
conn, err := toolsrv.Dial(socketPath, "")
|
conn, err := toolsrvclient.Dial(socketPath, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Dial with empty secret failed: %v", err)
|
t.Fatalf("Dial with empty secret failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -229,7 +231,7 @@ func TestIntegration_BasicOperations(t *testing.T) {
|
||||||
socketPath, cleanup := startTestServer(t)
|
socketPath, cleanup := startTestServer(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
conn, err := toolsrv.Dial(socketPath, "test-secret-ops")
|
conn, err := toolsrvclient.Dial(socketPath, "test-secret-ops")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Dial failed: %v", err)
|
t.Fatalf("Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -260,14 +262,14 @@ func TestIntegration_BasicOperations(t *testing.T) {
|
||||||
|
|
||||||
func TestIntegration_ToolExecution(t *testing.T) {
|
func TestIntegration_ToolExecution(t *testing.T) {
|
||||||
// Skip if tools directory doesn't exist (CI environment)
|
// 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)
|
t.Skipf("skipping: tools directory not available: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
socketPath, cleanup := startTestServer(t)
|
socketPath, cleanup := startTestServer(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
conn, err := toolsrv.Dial(socketPath, "test-secret-exec")
|
conn, err := toolsrvclient.Dial(socketPath, "test-secret-exec")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Dial failed: %v", err)
|
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
|
// 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 {
|
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
|
// Verify structure
|
||||||
if len(toolResult.Content) == 0 {
|
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" {
|
if toolResult.Content[0].Type != "text" {
|
||||||
t.Errorf("expected type 'text', got %q", toolResult.Content[0].Type)
|
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)
|
t.Errorf("output doesn't contain expected text: %q", toolResult.Content[0].Text)
|
||||||
}
|
}
|
||||||
if toolResult.IsError {
|
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)
|
t.Logf("Tool result: %+v", toolResult)
|
||||||
|
|
@ -328,14 +330,14 @@ func TestIntegration_ToolExecution(t *testing.T) {
|
||||||
|
|
||||||
func TestIntegration_MultilineContent(t *testing.T) {
|
func TestIntegration_MultilineContent(t *testing.T) {
|
||||||
// Skip if tools directory doesn't exist (CI environment)
|
// 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)
|
t.Skipf("skipping: tools directory not available: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
socketPath, cleanup := startTestServer(t)
|
socketPath, cleanup := startTestServer(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
conn, err := toolsrv.Dial(socketPath, "test-secret-multiline")
|
conn, err := toolsrvclient.Dial(socketPath, "test-secret-multiline")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Dial failed: %v", err)
|
t.Fatalf("Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -360,9 +362,9 @@ func TestIntegration_MultilineContent(t *testing.T) {
|
||||||
t.Fatalf("CallTool failed: %v", err)
|
t.Fatalf("CallTool failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var toolResult toolsrv.ToolResult
|
var toolResult protocol.ToolResult
|
||||||
if err := json.Unmarshal(result, &toolResult); err != nil {
|
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 {
|
if toolResult.IsError {
|
||||||
|
|
@ -382,14 +384,14 @@ func TestIntegration_MultilineContent(t *testing.T) {
|
||||||
|
|
||||||
func TestIntegration_ToolExecutionError(t *testing.T) {
|
func TestIntegration_ToolExecutionError(t *testing.T) {
|
||||||
// Skip if tools directory doesn't exist (CI environment)
|
// 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)
|
t.Skipf("skipping: tools directory not available: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
socketPath, cleanup := startTestServer(t)
|
socketPath, cleanup := startTestServer(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
conn, err := toolsrv.Dial(socketPath, "test-secret-exec-err")
|
conn, err := toolsrvclient.Dial(socketPath, "test-secret-exec-err")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Dial failed: %v", err)
|
t.Fatalf("Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -410,14 +412,14 @@ func TestIntegration_ToolExecutionError(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the result
|
// Parse the result
|
||||||
var toolResult toolsrv.ToolResult
|
var toolResult protocol.ToolResult
|
||||||
if err := json.Unmarshal(result, &toolResult); err != nil {
|
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
|
// Should be an error result with exit code info
|
||||||
if len(toolResult.Content) == 0 {
|
if len(toolResult.Content) == 0 {
|
||||||
t.Fatal("toolsrv.ToolResult.Content is empty")
|
t.Fatal("protocol.ToolResult.Content is empty")
|
||||||
}
|
}
|
||||||
if !toolResult.IsError {
|
if !toolResult.IsError {
|
||||||
t.Error("expected IsError=true for non-zero exit")
|
t.Error("expected IsError=true for non-zero exit")
|
||||||
|
|
@ -436,7 +438,7 @@ func TestIntegration_ConcurrentConnections(t *testing.T) {
|
||||||
secret := "test-secret-concurrent"
|
secret := "test-secret-concurrent"
|
||||||
|
|
||||||
// Establish the secret
|
// Establish the secret
|
||||||
conn0, err := toolsrv.Dial(socketPath, secret)
|
conn0, err := toolsrvclient.Dial(socketPath, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("initial Dial failed: %v", err)
|
t.Fatalf("initial Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -445,11 +447,11 @@ func TestIntegration_ConcurrentConnections(t *testing.T) {
|
||||||
|
|
||||||
// Open multiple concurrent connections
|
// Open multiple concurrent connections
|
||||||
const numConns = 5
|
const numConns = 5
|
||||||
conns := make([]*toolsrv.Conn, numConns)
|
conns := make([]*toolsrvclient.Conn, numConns)
|
||||||
errors := make([]error, numConns)
|
errors := make([]error, numConns)
|
||||||
|
|
||||||
for i := 0; i < numConns; i++ {
|
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
|
// Check all succeeded with same token
|
||||||
|
|
@ -478,7 +480,7 @@ func TestIntegration_ProcessKeeperRespawn(t *testing.T) {
|
||||||
socketPath1, cleanup1 := startTestServer(t)
|
socketPath1, cleanup1 := startTestServer(t)
|
||||||
|
|
||||||
secret1 := "secret-for-first-server"
|
secret1 := "secret-for-first-server"
|
||||||
conn1, err := toolsrv.Dial(socketPath1, secret1)
|
conn1, err := toolsrvclient.Dial(socketPath1, secret1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanup1()
|
cleanup1()
|
||||||
t.Fatalf("first Dial failed: %v", err)
|
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)
|
// New server should accept a new secret (any secret, since it's fresh)
|
||||||
secret2 := "secret-for-second-server"
|
secret2 := "secret-for-second-server"
|
||||||
conn2, err := toolsrv.Dial(socketPath2, secret2)
|
conn2, err := toolsrvclient.Dial(socketPath2, secret2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("second Dial failed: %v", err)
|
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
|
// Old secret should NOT work on new server
|
||||||
conn3, err := toolsrv.Dial(socketPath2, secret1)
|
conn3, err := toolsrvclient.Dial(socketPath2, secret1)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
conn3.Close()
|
conn3.Close()
|
||||||
t.Error("old secret should not work on new server")
|
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).
|
// call causes the call to return quickly (not wait for the full tool execution).
|
||||||
func TestIntegration_ToolCancellation(t *testing.T) {
|
func TestIntegration_ToolCancellation(t *testing.T) {
|
||||||
// Skip if tools directory doesn't exist (CI environment)
|
// 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)
|
t.Skipf("skipping: tools directory not available: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
socketPath, cleanup := startTestServer(t)
|
socketPath, cleanup := startTestServer(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
conn, err := toolsrv.Dial(socketPath, "test-secret-cancel")
|
conn, err := toolsrvclient.Dial(socketPath, "test-secret-cancel")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Dial failed: %v", err)
|
t.Fatalf("Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -588,14 +590,14 @@ func TestIntegration_ToolCancellation(t *testing.T) {
|
||||||
// take a long time to complete.
|
// take a long time to complete.
|
||||||
func TestIntegration_ToolCancellation_ContextPropagation(t *testing.T) {
|
func TestIntegration_ToolCancellation_ContextPropagation(t *testing.T) {
|
||||||
// Skip if tools directory doesn't exist (CI environment)
|
// 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)
|
t.Skipf("skipping: tools directory not available: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
socketPath, cleanup := startTestServer(t)
|
socketPath, cleanup := startTestServer(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
conn, err := toolsrv.Dial(socketPath, "test-secret-cancel-prop")
|
conn, err := toolsrvclient.Dial(socketPath, "test-secret-cancel-prop")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Dial failed: %v", err)
|
t.Fatalf("Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Bypass requests are submitted by sandboxed execution, exposed via 9P for
|
||||||
// external approval (olliesrv), and resolved when the approver responds.
|
// external approval (olliesrv), and resolved when the approver responds.
|
||||||
package bypass
|
package bypass
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ import (
|
||||||
"ollie/cmd/toolsrv/internal/bypass"
|
"ollie/cmd/toolsrv/internal/bypass"
|
||||||
"ollie/cmd/toolsrv/internal/sandbox"
|
"ollie/cmd/toolsrv/internal/sandbox"
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv/metadata"
|
||||||
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config contains all context needed for tool execution.
|
// 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.
|
// 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
|
// Resolve script path
|
||||||
toolPath, err := toolsrv.ResolveTool(info.Name)
|
toolPath, err := metadata.ResolveTool(info.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("resolve tool %s: %w", info.Name, err)
|
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 {
|
if err != nil {
|
||||||
return json.Marshal(toolsrv.ToolResult{
|
return json.Marshal(protocol.ToolResult{
|
||||||
IsError: true,
|
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{
|
return json.Marshal(protocol.ToolResult{
|
||||||
Content: []toolsrv.ToolResultContent{{Type: "text", Text: result}},
|
Content: []protocol.ToolResultContent{{Type: "text", Text: result}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,7 +145,7 @@ func executeSandboxed(ctx context.Context, toolPath, stdinData, cwd string, envE
|
||||||
envMap[k] = v
|
envMap[k] = v
|
||||||
}
|
}
|
||||||
// Prepend tools directory to PATH so meta-only cmd fields can reference other tools
|
// 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 != "" {
|
if existing := envMap["PATH"]; existing != "" {
|
||||||
envMap["PATH"] = toolsDir + ":" + existing
|
envMap["PATH"] = toolsDir + ":" + existing
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -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).
|
// Tools are scoped per-agent within a session (one toolsrv per session).
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
|
|
@ -7,28 +7,29 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv/metadata"
|
||||||
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Registry tracks loaded tools per agent.
|
// Registry tracks loaded tools per agent.
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
agents map[string]map[string]toolsrv.ToolInfo
|
agents map[string]map[string]protocol.ToolInfo
|
||||||
revisions map[string]uint64
|
revisions map[string]uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new tool registry.
|
// New creates a new tool registry.
|
||||||
func New() (*Registry, error) {
|
func New() (*Registry, error) {
|
||||||
return &Registry{
|
return &Registry{
|
||||||
agents: make(map[string]map[string]toolsrv.ToolInfo),
|
agents: make(map[string]map[string]protocol.ToolInfo),
|
||||||
revisions: make(map[string]uint64),
|
revisions: make(map[string]uint64),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// discover scans the tools directory and returns all valid tool infos.
|
// discover scans the tools directory and returns all valid tool infos.
|
||||||
func (r *Registry) discover() map[string]toolsrv.ToolInfo {
|
func (r *Registry) discover() map[string]protocol.ToolInfo {
|
||||||
infos := toolsrv.DiscoverTools()
|
infos := metadata.DiscoverTools()
|
||||||
tools := make(map[string]toolsrv.ToolInfo, len(infos))
|
tools := make(map[string]protocol.ToolInfo, len(infos))
|
||||||
for _, info := range infos {
|
for _, info := range infos {
|
||||||
tools[info.Name] = info
|
tools[info.Name] = info
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +48,7 @@ func (r *Registry) Load(agentID, name string) error {
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
if r.agents[agentID] == nil {
|
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
|
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.
|
// 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()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
|
@ -84,7 +85,7 @@ func (r *Registry) Loaded(agentID string) []toolsrv.ToolInfo {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var loaded []toolsrv.ToolInfo
|
var loaded []protocol.ToolInfo
|
||||||
for _, info := range agentTools {
|
for _, info := range agentTools {
|
||||||
loaded = append(loaded, info)
|
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.
|
// 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()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
agentTools, ok := r.agents[agentID]
|
agentTools, ok := r.agents[agentID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return toolsrv.ToolInfo{}, false
|
return protocol.ToolInfo{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
tool, exists := agentTools[name]
|
tool, exists := agentTools[name]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// proc.go - Process management and tool operations for toolsrv.
|
// proc.go - Process management and tool operations for toolsrvclient.
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -19,10 +19,10 @@ import (
|
||||||
"ollie/cmd/toolsrv/internal/exec"
|
"ollie/cmd/toolsrv/internal/exec"
|
||||||
"ollie/cmd/toolsrv/internal/registry"
|
"ollie/cmd/toolsrv/internal/registry"
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
// State implements the filesystem state for toolsrv.
|
// State implements the filesystem state for toolsrvclient.
|
||||||
type State struct {
|
type State struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ func (st *State) GetEnv(key string) string {
|
||||||
// --- Tool Registry ---
|
// --- Tool Registry ---
|
||||||
|
|
||||||
// ListTools returns loaded tools for the given agent.
|
// 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 == "" {
|
if agentID == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +217,7 @@ func (st *State) allocID() int {
|
||||||
// If background is true, returns immediately with pid.
|
// 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) {
|
func (st *State) NewProc(ctx context.Context, payload string, background bool) (result string, pid int, err error) {
|
||||||
// Parse payload
|
// Parse payload
|
||||||
args := toolsrv.ParsePayload(payload)
|
args := protocol.ParsePayload(payload)
|
||||||
toolName := args["tool"]
|
toolName := args["tool"]
|
||||||
if toolName == "" {
|
if toolName == "" {
|
||||||
return "", 0, fmt.Errorf("missing 'tool' in payload")
|
return "", 0, fmt.Errorf("missing 'tool' in payload")
|
||||||
|
|
@ -378,7 +378,7 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) (
|
||||||
|
|
||||||
// executeTool runs a tool and returns output + exit code.
|
// executeTool runs a tool and returns output + exit code.
|
||||||
// timeout: 0 = no timeout, >0 = seconds.
|
// 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
|
// Convert args map to JSON for the execution path
|
||||||
jsonArgs := argsToJSON(args)
|
jsonArgs := argsToJSON(args)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestState_Basic(t *testing.T) {
|
func TestState_Basic(t *testing.T) {
|
||||||
|
|
@ -191,10 +191,10 @@ func TestParsePayload(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got := toolsrv.ParsePayload(tt.input)
|
got := protocol.ParsePayload(tt.input)
|
||||||
for k, v := range tt.want {
|
for k, v := range tt.want {
|
||||||
if got[k] != v {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// spec.go - virtfs-based namespace specification for toolsrv.
|
// spec.go - virtfs-based namespace specification for toolsrvclient.
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
|
|
||||||
"ollie/cmd/toolsrv/internal/bypass"
|
"ollie/cmd/toolsrv/internal/bypass"
|
||||||
"ollie/cmd/toolsrv/internal/registry"
|
"ollie/cmd/toolsrv/internal/registry"
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv/metadata"
|
||||||
"ollie/virtfs"
|
"ollie/virtfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -129,7 +129,7 @@ func Spec(srv *Server) virtfs.FsNodeDecl {
|
||||||
virtfs.FileNode("all", 0444,
|
virtfs.FileNode("all", 0444,
|
||||||
virtfs.Doc("All available tools on disk: name<tab>description per line"),
|
virtfs.Doc("All available tools on disk: name<tab>description per line"),
|
||||||
virtfs.Read(func() ([]byte, error) {
|
virtfs.Read(func() ([]byte, error) {
|
||||||
tools := toolsrv.DiscoverTools()
|
tools := metadata.DiscoverTools()
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, ti := range tools {
|
for _, ti := range tools {
|
||||||
fmt.Fprintf(&sb, "%s\t%s\n", ti.Name, ti.Description)
|
fmt.Fprintf(&sb, "%s\t%s\n", ti.Name, ti.Description)
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ollie/cmd/toolsrv/internal/server"
|
|
||||||
"ollie/cmd/toolsrv/internal/registry"
|
"ollie/cmd/toolsrv/internal/registry"
|
||||||
|
"ollie/cmd/toolsrv/internal/server"
|
||||||
"ollie/env"
|
"ollie/env"
|
||||||
lib9p "ollie/lib9p/client"
|
lib9p "ollie/lib9p/client"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// p9.go - 9P filesystem server for toolsrv.
|
// p9.go - 9P filesystem server for toolsrvclient.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// client9p.go - 9P client for toolsrv.
|
// client9p.go - 9P client for toolsrvclient.
|
||||||
package toolsrv
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -12,9 +12,11 @@ import (
|
||||||
|
|
||||||
"9fans.net/go/plan9"
|
"9fans.net/go/plan9"
|
||||||
p9client "9fans.net/go/plan9/client"
|
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 {
|
type Conn struct {
|
||||||
fsys *p9client.Fsys
|
fsys *p9client.Fsys
|
||||||
conn *p9client.Conn
|
conn *p9client.Conn
|
||||||
|
|
@ -102,7 +104,7 @@ func (c *Conn) Token() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListTools returns the list of loaded tools for this agent.
|
// 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)
|
fid, err := c.fsys.Open("tools", plan9.ORDWR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err := json.Unmarshal(result, &tools); err != nil {
|
||||||
return nil, fmt.Errorf("parse tools: %w", err)
|
return nil, fmt.Errorf("parse tools: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +247,7 @@ func (c *Conn) LoadTool(name string) error {
|
||||||
return err
|
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) {
|
func (c *Conn) ListAllTools() ([]byte, error) {
|
||||||
fid, err := c.fsys.Open("all", plan9.OREAD)
|
fid, err := c.fsys.Open("all", plan9.OREAD)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -475,7 +477,7 @@ func (c *Conn) GetDetachedOutput(pid int) (string, error) {
|
||||||
|
|
||||||
// --- Helper types ---
|
// --- Helper types ---
|
||||||
|
|
||||||
// HostInfo contains information about the host running toolsrv.
|
// HostInfo contains information about the host running toolsrvclient.
|
||||||
type HostInfo struct {
|
type HostInfo struct {
|
||||||
Platform string
|
Platform string
|
||||||
IsGitRepo bool
|
IsGitRepo bool
|
||||||
|
|
@ -539,18 +541,10 @@ func escapeValue(s string) string {
|
||||||
return string(result)
|
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.
|
// ReadBypassPending blocks until a bypass request is available.
|
||||||
// This should be called in a loop by the approval handler.
|
// This should be called in a loop by the approval handler.
|
||||||
// Returns nil, ctx.Err() if context is cancelled.
|
// 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)
|
fid, err := c.fsys.Open("bypass/pending", plan9.OREAD)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open bypass/pending: %w", err)
|
return nil, fmt.Errorf("open bypass/pending: %w", err)
|
||||||
|
|
@ -588,7 +582,7 @@ func (c *Conn) ReadBypassPending(ctx context.Context) (*BypassRequest, error) {
|
||||||
}
|
}
|
||||||
fid.Close()
|
fid.Close()
|
||||||
|
|
||||||
var req BypassRequest
|
var req protocol.BypassRequest
|
||||||
if err := json.Unmarshal(result, &req); err != nil {
|
if err := json.Unmarshal(result, &req); err != nil {
|
||||||
return nil, fmt.Errorf("parse bypass request: %w", err)
|
return nil, fmt.Errorf("parse bypass request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package toolsrv
|
package metadata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"ollie/toolsrv/protocol"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -162,10 +163,10 @@ func checkCondition(key, val string) bool {
|
||||||
|
|
||||||
// ToolInfoFromMeta builds a ToolInfo from a MetaFile.
|
// ToolInfoFromMeta builds a ToolInfo from a MetaFile.
|
||||||
// Applies variant resolution first. Returns zero ToolInfo if no variant matches.
|
// 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()
|
resolved := m.Resolve()
|
||||||
if resolved == nil {
|
if resolved == nil {
|
||||||
return ToolInfo{}
|
return protocol.ToolInfo{}
|
||||||
}
|
}
|
||||||
args := resolved.Args
|
args := resolved.Args
|
||||||
if args == nil {
|
if args == nil {
|
||||||
|
|
@ -175,7 +176,7 @@ func ToolInfoFromMeta(name string, m *MetaFile) ToolInfo {
|
||||||
if tier == "" {
|
if tier == "" {
|
||||||
tier = "hot"
|
tier = "hot"
|
||||||
}
|
}
|
||||||
return ToolInfo{
|
return protocol.ToolInfo{
|
||||||
Name: name,
|
Name: name,
|
||||||
Description: resolved.Description,
|
Description: resolved.Description,
|
||||||
InputSchema: args,
|
InputSchema: args,
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// parse.go — Shared key=value payload parsing.
|
// parse.go — Shared key=value payload parsing.
|
||||||
package toolsrv
|
package protocol
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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"`
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue