chore: remove unused legacy bridge helpers
This commit is contained in:
parent
e2f005537d
commit
217e2665ff
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
build/
|
||||
dist/
|
||||
.env
|
||||
xworkmate-bridge
|
||||
xworkmate-go-core-linux
|
||||
|
||||
@ -13,13 +13,6 @@ type CapabilityCatalog struct {
|
||||
ProviderProbeSummary []any `json:"providerProbeSummary"`
|
||||
}
|
||||
|
||||
func (c *CapabilityCatalog) Update(providers []any, targets []any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.ProviderCatalog = providers
|
||||
c.AvailableExecutionTargets = targets
|
||||
}
|
||||
|
||||
func (c *CapabilityCatalog) Get() map[string]any {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"xworkmate-bridge/internal/service"
|
||||
)
|
||||
|
||||
type Authenticator interface {
|
||||
Authenticate(username, password string) error
|
||||
}
|
||||
|
||||
type AuthHandler struct {
|
||||
service Authenticator
|
||||
}
|
||||
|
||||
func NewAuthHandler(svc Authenticator) *AuthHandler {
|
||||
return &AuthHandler{service: svc}
|
||||
}
|
||||
|
||||
func NewServiceAdapter(svc *service.AuthService) Authenticator {
|
||||
return authServiceAdapter{service: svc}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.service.Authenticate(payload.Username, payload.Password); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
type authServiceAdapter struct {
|
||||
service *service.AuthService
|
||||
}
|
||||
|
||||
func (a authServiceAdapter) Authenticate(username, password string) error {
|
||||
return a.service.Authenticate(context.TODO(), username, password)
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeAuthenticator struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeAuthenticator) Authenticate(username, password string) error {
|
||||
return f.err
|
||||
}
|
||||
|
||||
func TestAuthHandlerRejectsInvalidJSON(t *testing.T) {
|
||||
handler := NewAuthHandler(fakeAuthenticator{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString("{"))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHandlerReturnsUnauthorizedOnServiceFailure(t *testing.T) {
|
||||
handler := NewAuthHandler(fakeAuthenticator{err: errors.New("invalid credentials")})
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString(`{"username":"alice","password":"secret"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHandlerReturnsOKOnSuccess(t *testing.T) {
|
||||
handler := NewAuthHandler(fakeAuthenticator{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString(`{"username":"alice","password":"secret"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"xworkmate-bridge/internal/service"
|
||||
"xworkmate-bridge/internal/shared"
|
||||
)
|
||||
|
||||
type TokenAuthHandler struct {
|
||||
service *service.StaticTokenAuthService
|
||||
}
|
||||
|
||||
func NewTokenAuthHandler(service *service.StaticTokenAuthService) *TokenAuthHandler {
|
||||
return &TokenAuthHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *TokenAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if h.service == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
_ = json.NewEncoder(w).Encode(shared.ErrorEnvelope(nil, -32000, "auth service unavailable"))
|
||||
return
|
||||
}
|
||||
token := r.Header.Get("Authorization")
|
||||
if h.service.ValidateAuthorizationHeader(token) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"ok": true,
|
||||
"type": "res",
|
||||
"payload": map[string]any{"authenticated": true},
|
||||
})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = json.NewEncoder(w).Encode(shared.ErrorEnvelope(nil, -32001, "unauthorized"))
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"xworkmate-bridge/internal/service"
|
||||
)
|
||||
|
||||
func TestTokenAuthHandlerServeHTTP(t *testing.T) {
|
||||
h := NewTokenAuthHandler(service.NewStaticTokenAuthService("secret"))
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "secret")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenAuthHandlerRejectsUnauthorized(t *testing.T) {
|
||||
h := NewTokenAuthHandler(service.NewStaticTokenAuthService("secret"))
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
@ -1,146 +0,0 @@
|
||||
package mounts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
codexManagedMCPBlockStart = "# BEGIN XWORKMATE MANAGED MCP BLOCK"
|
||||
codexManagedMCPBlockEnd = "# END XWORKMATE MANAGED MCP BLOCK"
|
||||
opencodeManagedMCPBlockStart = "# BEGIN XWORKMATE MANAGED MCP BLOCK"
|
||||
opencodeManagedMCPBlockEnd = "# END XWORKMATE MANAGED MCP BLOCK"
|
||||
)
|
||||
|
||||
var mcpServerSectionPattern = regexp.MustCompile(
|
||||
`(?m)^\[mcp_servers\.[^\]]+\]`,
|
||||
)
|
||||
|
||||
func countMCPSections(content string) int {
|
||||
return len(mcpServerSectionPattern.FindAllStringIndex(content, -1))
|
||||
}
|
||||
|
||||
func defaultCodexHome() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || strings.TrimSpace(home) == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".codex")
|
||||
}
|
||||
|
||||
func defaultOpencodeHome() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || strings.TrimSpace(home) == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".opencode")
|
||||
}
|
||||
|
||||
func defaultOpenClawHome() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || strings.TrimSpace(home) == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".openclaw")
|
||||
}
|
||||
|
||||
func stripManagedBlock(content, startMarker, endMarker string) string {
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return content
|
||||
}
|
||||
|
||||
remaining := content
|
||||
for {
|
||||
start := strings.Index(remaining, startMarker)
|
||||
if start < 0 {
|
||||
break
|
||||
}
|
||||
end := strings.Index(remaining[start:], endMarker)
|
||||
if end < 0 {
|
||||
remaining = remaining[:start]
|
||||
break
|
||||
}
|
||||
end += start
|
||||
remaining = remaining[:start] + remaining[end+len(endMarker):]
|
||||
}
|
||||
return remaining
|
||||
}
|
||||
|
||||
func mergeManagedBlock(content, block, startMarker, endMarker string) string {
|
||||
preserved := strings.TrimRight(
|
||||
stripManagedBlock(content, startMarker, endMarker),
|
||||
"\n",
|
||||
)
|
||||
if preserved == "" {
|
||||
return block + "\n"
|
||||
}
|
||||
return preserved + "\n\n" + block + "\n"
|
||||
}
|
||||
|
||||
func buildCodexManagedMCPBlock(servers []ManagedMCPServer) string {
|
||||
var buffer strings.Builder
|
||||
buffer.WriteString(codexManagedMCPBlockStart)
|
||||
buffer.WriteString("\n# Generated by XWorkmate - Managed MCP Server Configuration\n")
|
||||
_, _ = fmt.Fprintf(&buffer, "# Last updated: %s\n\n", time.Now().Format(time.RFC3339Nano))
|
||||
for _, server := range servers {
|
||||
_, _ = fmt.Fprintf(&buffer, "[mcp_servers.%s]\n", server.ID)
|
||||
_, _ = fmt.Fprintf(&buffer, "command = %q\n", server.Command)
|
||||
if len(server.Args) > 0 {
|
||||
_, _ = fmt.Fprintf(&buffer, "args = %s\n", formatTOMLArray(server.Args))
|
||||
}
|
||||
buffer.WriteString("\n")
|
||||
}
|
||||
buffer.WriteString(codexManagedMCPBlockEnd)
|
||||
return strings.TrimRight(buffer.String(), "\n")
|
||||
}
|
||||
|
||||
func buildOpencodeManagedMCPBlock(servers []ManagedMCPServer) string {
|
||||
var buffer strings.Builder
|
||||
buffer.WriteString(opencodeManagedMCPBlockStart)
|
||||
buffer.WriteString("\n# Generated by XWorkmate - Managed MCP Server Configuration\n")
|
||||
_, _ = fmt.Fprintf(&buffer, "# Last updated: %s\n\n", time.Now().Format(time.RFC3339Nano))
|
||||
for _, server := range servers {
|
||||
_, _ = fmt.Fprintf(&buffer, "[mcp_servers.%s]\n", server.ID)
|
||||
if strings.TrimSpace(server.URL) != "" {
|
||||
_, _ = fmt.Fprintf(&buffer, "url = %q\n", strings.TrimSpace(server.URL))
|
||||
} else {
|
||||
buffer.WriteString("type = \"stdio\"\n")
|
||||
_, _ = fmt.Fprintf(&buffer, "command = %q\n", server.Command)
|
||||
if len(server.Args) > 0 {
|
||||
_, _ = fmt.Fprintf(&buffer, "args = %s\n", formatTOMLArray(server.Args))
|
||||
}
|
||||
}
|
||||
buffer.WriteString("\n")
|
||||
}
|
||||
buffer.WriteString(opencodeManagedMCPBlockEnd)
|
||||
return strings.TrimRight(buffer.String(), "\n")
|
||||
}
|
||||
|
||||
func formatTOMLArray(items []string) string {
|
||||
if len(items) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
var quoted []string
|
||||
for _, item := range items {
|
||||
quoted = append(quoted, fmt.Sprintf("%q", item))
|
||||
}
|
||||
return "[" + strings.Join(quoted, ", ") + "]"
|
||||
}
|
||||
|
||||
func applyManagedBlock(configPath, block, startMarker, endMarker string) error {
|
||||
configDir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
merged := mergeManagedBlock(string(content), block, startMarker, endMarker)
|
||||
return os.WriteFile(configPath, []byte(merged), 0o644)
|
||||
}
|
||||
@ -1,444 +0,0 @@
|
||||
package mounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ManagedMCPServer struct {
|
||||
ID string
|
||||
Name string
|
||||
Transport string
|
||||
Command string
|
||||
URL string
|
||||
Args []string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
AutoSync bool
|
||||
UsesAris bool
|
||||
ManagedMCPServers []ManagedMCPServer
|
||||
}
|
||||
|
||||
type ArisInput struct {
|
||||
Available bool
|
||||
BundleVersion string
|
||||
LLMChatServerPath string
|
||||
SkillCount int
|
||||
BridgeAvailable bool
|
||||
Error string
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Config Config
|
||||
AIGatewayURL string
|
||||
ConfiguredCodexCLIPath string
|
||||
CodexHome string
|
||||
OpencodeHome string
|
||||
OpenClawHome string
|
||||
Aris ArisInput
|
||||
}
|
||||
|
||||
type MountTargetState struct {
|
||||
TargetID string
|
||||
Label string
|
||||
Available bool
|
||||
SupportsSkills bool
|
||||
SupportsMCP bool
|
||||
SupportsAIGatewayInjection bool
|
||||
DiscoveryState string
|
||||
SyncState string
|
||||
DiscoveredSkillCount int
|
||||
DiscoveredMCPCount int
|
||||
ManagedMCPCount int
|
||||
Detail string
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
MountTargets []MountTargetState
|
||||
ArisBundleVersion string
|
||||
ArisCompatStatus string
|
||||
}
|
||||
|
||||
func Reconcile(request Request) Result {
|
||||
states := []MountTargetState{
|
||||
reconcileAris(request.Config, request.Aris),
|
||||
reconcileCodex(
|
||||
request.Config,
|
||||
request.AIGatewayURL,
|
||||
request.ConfiguredCodexCLIPath,
|
||||
request.CodexHome,
|
||||
),
|
||||
reconcileCLIListTarget(
|
||||
request.Config,
|
||||
"claude",
|
||||
"Claude",
|
||||
[]string{"claude", "mcp", "list"},
|
||||
),
|
||||
reconcileCLIListTarget(
|
||||
request.Config,
|
||||
"gemini",
|
||||
"Gemini",
|
||||
[]string{"gemini", "mcp", "list"},
|
||||
),
|
||||
reconcileOpencode(request.Config, request.OpencodeHome),
|
||||
reconcileOpenClaw(request.Config, request.OpenClawHome),
|
||||
}
|
||||
|
||||
result := Result{
|
||||
MountTargets: states,
|
||||
ArisBundleVersion: strings.TrimSpace(request.Aris.BundleVersion),
|
||||
ArisCompatStatus: "idle",
|
||||
}
|
||||
for _, state := range states {
|
||||
if state.TargetID == "aris" {
|
||||
result.ArisCompatStatus = state.SyncState
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ResultMap(result Result) map[string]any {
|
||||
rawTargets := make([]map[string]any, 0, len(result.MountTargets))
|
||||
for _, target := range result.MountTargets {
|
||||
rawTargets = append(rawTargets, map[string]any{
|
||||
"targetId": target.TargetID,
|
||||
"label": target.Label,
|
||||
"available": target.Available,
|
||||
"supportsSkills": target.SupportsSkills,
|
||||
"supportsMcp": target.SupportsMCP,
|
||||
"supportsAiGatewayInjection": target.SupportsAIGatewayInjection,
|
||||
"discoveryState": target.DiscoveryState,
|
||||
"syncState": target.SyncState,
|
||||
"discoveredSkillCount": target.DiscoveredSkillCount,
|
||||
"discoveredMcpCount": target.DiscoveredMCPCount,
|
||||
"managedMcpCount": target.ManagedMCPCount,
|
||||
"detail": target.Detail,
|
||||
})
|
||||
}
|
||||
return map[string]any{
|
||||
"mountTargets": rawTargets,
|
||||
"arisBundleVersion": result.ArisBundleVersion,
|
||||
"arisCompatStatus": result.ArisCompatStatus,
|
||||
}
|
||||
}
|
||||
|
||||
func reconcileAris(config Config, input ArisInput) MountTargetState {
|
||||
state := placeholderState("aris", "ARIS", true, true, false)
|
||||
if strings.TrimSpace(input.Error) != "" {
|
||||
state.Available = false
|
||||
state.DiscoveryState = "error"
|
||||
state.SyncState = "error"
|
||||
state.Detail = strings.TrimSpace(input.Error)
|
||||
return state
|
||||
}
|
||||
if !input.Available {
|
||||
state.DiscoveryState = "missing"
|
||||
state.SyncState = "missing"
|
||||
state.Detail = "Embedded ARIS bundle is unavailable."
|
||||
return state
|
||||
}
|
||||
|
||||
state.Available = true
|
||||
state.DiscoveryState = "ready"
|
||||
state.DiscoveredSkillCount = input.SkillCount
|
||||
llmChatReady := strings.TrimSpace(input.LLMChatServerPath) != ""
|
||||
if config.UsesAris && llmChatReady && input.BridgeAvailable {
|
||||
state.SyncState = "ready"
|
||||
state.DiscoveredMCPCount = 1
|
||||
state.ManagedMCPCount = 1
|
||||
state.Detail = "Embedded bundle " +
|
||||
strings.TrimSpace(input.BundleVersion) +
|
||||
" ready; XWorkmate Go core manages llm-chat and claude-review."
|
||||
return state
|
||||
}
|
||||
state.SyncState = "embedded"
|
||||
if llmChatReady {
|
||||
state.DiscoveredMCPCount = 1
|
||||
}
|
||||
if llmChatReady {
|
||||
state.Detail = "Embedded bundle extracted, but the XWorkmate Go core is not available yet."
|
||||
} else {
|
||||
state.Detail = "Embedded bundle extracted, but llm-chat metadata is missing."
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func reconcileCodex(
|
||||
config Config,
|
||||
aiGatewayURL string,
|
||||
configuredCodexCLIPath string,
|
||||
codexHome string,
|
||||
) MountTargetState {
|
||||
state := placeholderState("codex", "Codex", true, true, true)
|
||||
available := codexAvailable(configuredCodexCLIPath)
|
||||
configHome := strings.TrimSpace(codexHome)
|
||||
if configHome == "" {
|
||||
configHome = defaultCodexHome()
|
||||
}
|
||||
configPath := filepath.Join(configHome, "config.toml")
|
||||
content, _ := os.ReadFile(configPath)
|
||||
discovered := countMCPSections(string(content))
|
||||
managedServers := enabledCodexServers(config.ManagedMCPServers)
|
||||
if available && config.AutoSync && len(managedServers) > 0 {
|
||||
_ = applyManagedBlock(
|
||||
configPath,
|
||||
buildCodexManagedMCPBlock(managedServers),
|
||||
codexManagedMCPBlockStart,
|
||||
codexManagedMCPBlockEnd,
|
||||
)
|
||||
}
|
||||
state.Available = available
|
||||
if available {
|
||||
state.DiscoveryState = "ready"
|
||||
} else {
|
||||
state.DiscoveryState = "missing"
|
||||
}
|
||||
switch {
|
||||
case !available:
|
||||
state.SyncState = "missing"
|
||||
case config.AutoSync:
|
||||
state.SyncState = "ready"
|
||||
default:
|
||||
state.SyncState = "disabled"
|
||||
}
|
||||
state.DiscoveredMCPCount = discovered
|
||||
state.ManagedMCPCount = len(managedServers)
|
||||
state.Detail = "Codex is exposed through the bridge control plane.\n" +
|
||||
"Canonical WebSocket endpoint: https://xworkmate-bridge.svc.plus/acp\n" +
|
||||
"Secondary HTTP RPC endpoint: https://xworkmate-bridge.svc.plus/acp/rpc"
|
||||
return state
|
||||
}
|
||||
|
||||
func reconcileCLIListTarget(
|
||||
config Config,
|
||||
targetID string,
|
||||
label string,
|
||||
command []string,
|
||||
) MountTargetState {
|
||||
state := placeholderState(targetID, label, true, true, true)
|
||||
available := binaryExists(command[0])
|
||||
discovered := 0
|
||||
if available {
|
||||
discovered = countListedEntries(command)
|
||||
}
|
||||
state.Available = available
|
||||
if available {
|
||||
state.DiscoveryState = "ready"
|
||||
} else {
|
||||
state.DiscoveryState = "missing"
|
||||
}
|
||||
if available && config.AutoSync {
|
||||
state.SyncState = "launch-only"
|
||||
} else {
|
||||
state.SyncState = "disabled"
|
||||
}
|
||||
state.DiscoveredMCPCount = discovered
|
||||
state.ManagedMCPCount = len(enabledServers(config.ManagedMCPServers))
|
||||
|
||||
if targetID == "gemini" {
|
||||
state.Detail = "Gemini is exposed through the bridge control plane.\n" +
|
||||
"Canonical WebSocket endpoint: https://xworkmate-bridge.svc.plus/acp\n" +
|
||||
"Secondary HTTP RPC endpoint: https://xworkmate-bridge.svc.plus/acp/rpc"
|
||||
} else {
|
||||
state.Detail = "MCP discovery uses `" + strings.Join(command, " ") +
|
||||
"`; LLM API stays launch-scoped."
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func reconcileOpencode(config Config, opencodeHome string) MountTargetState {
|
||||
state := placeholderState("opencode", "OpenCode", true, true, true)
|
||||
available := binaryExists("opencode")
|
||||
configHome := strings.TrimSpace(opencodeHome)
|
||||
if configHome == "" {
|
||||
configHome = defaultOpencodeHome()
|
||||
}
|
||||
configPath := filepath.Join(configHome, "config.toml")
|
||||
content, _ := os.ReadFile(configPath)
|
||||
discovered := countMCPSections(string(content))
|
||||
managedServers := enabledServers(config.ManagedMCPServers)
|
||||
if available && config.AutoSync && len(managedServers) > 0 {
|
||||
_ = applyManagedBlock(
|
||||
configPath,
|
||||
buildOpencodeManagedMCPBlock(managedServers),
|
||||
opencodeManagedMCPBlockStart,
|
||||
opencodeManagedMCPBlockEnd,
|
||||
)
|
||||
}
|
||||
state.Available = available
|
||||
if available {
|
||||
state.DiscoveryState = "ready"
|
||||
} else {
|
||||
state.DiscoveryState = "missing"
|
||||
}
|
||||
switch {
|
||||
case !available:
|
||||
state.SyncState = "missing"
|
||||
case config.AutoSync:
|
||||
state.SyncState = "ready"
|
||||
default:
|
||||
state.SyncState = "disabled"
|
||||
}
|
||||
state.DiscoveredMCPCount = discovered
|
||||
state.ManagedMCPCount = len(managedServers)
|
||||
state.Detail = "OpenCode is exposed through the bridge control plane.\n" +
|
||||
"Canonical WebSocket endpoint: https://xworkmate-bridge.svc.plus/acp\n" +
|
||||
"Secondary HTTP RPC endpoint: https://xworkmate-bridge.svc.plus/acp/rpc"
|
||||
return state
|
||||
}
|
||||
|
||||
func reconcileOpenClaw(config Config, openClawHome string) MountTargetState {
|
||||
state := placeholderState("openclaw", "OpenClaw", true, false, true)
|
||||
available := binaryExists("openclaw")
|
||||
state.Available = available
|
||||
if available {
|
||||
state.DiscoveryState = "ready"
|
||||
} else {
|
||||
state.DiscoveryState = "missing"
|
||||
}
|
||||
if available && config.AutoSync {
|
||||
state.SyncState = "launch-only"
|
||||
} else {
|
||||
state.SyncState = "disabled"
|
||||
}
|
||||
state.Detail = "OpenClaw acts as the host/control plane mount."
|
||||
|
||||
configHome := strings.TrimSpace(openClawHome)
|
||||
if configHome == "" {
|
||||
configHome = defaultOpenClawHome()
|
||||
}
|
||||
configPath := filepath.Join(configHome, "openclaw.json")
|
||||
if content, err := os.ReadFile(configPath); err == nil {
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(content, &decoded); err == nil {
|
||||
agents := 0
|
||||
if rawAgents, ok := decoded["agents"].(map[string]any); ok {
|
||||
if rawList, ok := rawAgents["list"].([]any); ok {
|
||||
agents = len(rawList)
|
||||
}
|
||||
}
|
||||
skillsDir := filepath.Join(configHome, "skills")
|
||||
if entries, err := os.ReadDir(skillsDir); err == nil {
|
||||
state.DiscoveredSkillCount = len(entries)
|
||||
}
|
||||
state.Detail = "agents: " + itoa(agents) + " · skills: " +
|
||||
itoa(state.DiscoveredSkillCount)
|
||||
} else {
|
||||
state.Detail = "OpenClaw config detected but could not be fully parsed."
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func placeholderState(
|
||||
targetID string,
|
||||
label string,
|
||||
supportsSkills bool,
|
||||
supportsMCP bool,
|
||||
supportsAIGatewayInjection bool,
|
||||
) MountTargetState {
|
||||
return MountTargetState{
|
||||
TargetID: targetID,
|
||||
Label: label,
|
||||
SupportsSkills: supportsSkills,
|
||||
SupportsMCP: supportsMCP,
|
||||
SupportsAIGatewayInjection: supportsAIGatewayInjection,
|
||||
DiscoveryState: "idle",
|
||||
SyncState: "idle",
|
||||
}
|
||||
}
|
||||
|
||||
func codexAvailable(configuredPath string) bool {
|
||||
if strings.TrimSpace(configuredPath) != "" {
|
||||
if _, err := os.Stat(strings.TrimSpace(configuredPath)); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return binaryExists("codex")
|
||||
}
|
||||
|
||||
func binaryExists(command string) bool {
|
||||
_, err := exec.LookPath(command)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func countListedEntries(command []string) int {
|
||||
output := strings.TrimSpace(runCommand(command))
|
||||
if output == "" ||
|
||||
strings.Contains(output, "No MCP servers configured") ||
|
||||
strings.Contains(output, "No MCP servers configured yet") ||
|
||||
strings.Contains(output, "No MCP servers configured.") {
|
||||
return 0
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
count := 0
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
switch {
|
||||
case trimmed == "":
|
||||
case strings.HasPrefix(trimmed, "Usage:"):
|
||||
case strings.HasPrefix(trimmed, "┌"):
|
||||
case strings.HasPrefix(trimmed, "│"):
|
||||
case strings.HasPrefix(trimmed, "└"):
|
||||
default:
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func runCommand(command []string) string {
|
||||
if len(command) == 0 {
|
||||
return ""
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil && len(output) == 0 {
|
||||
return ""
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func enabledServers(servers []ManagedMCPServer) []ManagedMCPServer {
|
||||
filtered := make([]ManagedMCPServer, 0, len(servers))
|
||||
for _, server := range servers {
|
||||
if !server.Enabled {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, server)
|
||||
}
|
||||
sort.SliceStable(filtered, func(i, j int) bool {
|
||||
return filtered[i].ID < filtered[j].ID
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
|
||||
func enabledCodexServers(servers []ManagedMCPServer) []ManagedMCPServer {
|
||||
filtered := make([]ManagedMCPServer, 0, len(servers))
|
||||
for _, server := range servers {
|
||||
if !server.Enabled || strings.TrimSpace(server.Command) == "" {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, server)
|
||||
}
|
||||
sort.SliceStable(filtered, func(i, j int) bool {
|
||||
return filtered[i].ID < filtered[j].ID
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
|
||||
func itoa(value int) string {
|
||||
return strconv.Itoa(value)
|
||||
}
|
||||
@ -1,115 +0,0 @@
|
||||
package mounts
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReconcileCodexAppliesManagedBlockAndPreservesUserEntries(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
configuredBinary := filepath.Join(tempDir, "custom-codex")
|
||||
if err := os.WriteFile(configuredBinary, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatalf("write configured binary: %v", err)
|
||||
}
|
||||
configPath := filepath.Join(tempDir, "config.toml")
|
||||
if err := os.WriteFile(configPath, []byte(`
|
||||
[mcp_servers.user_server]
|
||||
command = "user-mcp"
|
||||
`), 0o644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
result := Reconcile(Request{
|
||||
Config: Config{
|
||||
AutoSync: true,
|
||||
ManagedMCPServers: []ManagedMCPServer{
|
||||
{ID: "xworkmate_server", Command: "xworkmate-mcp", Args: []string{"--port", "7777"}, Enabled: true},
|
||||
},
|
||||
},
|
||||
ConfiguredCodexCLIPath: configuredBinary,
|
||||
CodexHome: tempDir,
|
||||
})
|
||||
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read config: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(content), `[mcp_servers.user_server]`) {
|
||||
t.Fatalf("expected user entry preserved: %s", string(content))
|
||||
}
|
||||
if !strings.Contains(string(content), `[mcp_servers.xworkmate_server]`) {
|
||||
t.Fatalf("expected managed entry written: %s", string(content))
|
||||
}
|
||||
if strings.Count(string(content), codexManagedMCPBlockStart) != 1 {
|
||||
t.Fatalf("expected single managed block: %s", string(content))
|
||||
}
|
||||
if result.MountTargets[1].ManagedMCPCount != 1 {
|
||||
t.Fatalf("expected codex managed count 1, got %d", result.MountTargets[1].ManagedMCPCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileOpencodeAppliesManagedBlockAndPreservesUserEntries(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
binDir := t.TempDir()
|
||||
originalPath := os.Getenv("PATH")
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+originalPath)
|
||||
if err := os.WriteFile(filepath.Join(binDir, "opencode"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatalf("write opencode binary: %v", err)
|
||||
}
|
||||
configPath := filepath.Join(tempDir, "config.toml")
|
||||
if err := os.WriteFile(configPath, []byte(`
|
||||
[model]
|
||||
name = "user-default"
|
||||
`), 0o644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
result := Reconcile(Request{
|
||||
Config: Config{
|
||||
AutoSync: true,
|
||||
ManagedMCPServers: []ManagedMCPServer{
|
||||
{ID: "xworkmate_server", Command: "xworkmate-mcp", Args: []string{"--port", "3001"}, Enabled: true},
|
||||
},
|
||||
},
|
||||
OpencodeHome: tempDir,
|
||||
})
|
||||
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read config: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(content), `[model]`) {
|
||||
t.Fatalf("expected user config preserved: %s", string(content))
|
||||
}
|
||||
if !strings.Contains(string(content), `[mcp_servers.xworkmate_server]`) {
|
||||
t.Fatalf("expected managed opencode entry written: %s", string(content))
|
||||
}
|
||||
if strings.Count(string(content), opencodeManagedMCPBlockStart) != 1 {
|
||||
t.Fatalf("expected single opencode managed block: %s", string(content))
|
||||
}
|
||||
if result.MountTargets[4].ManagedMCPCount != 1 {
|
||||
t.Fatalf("expected opencode managed count 1, got %d", result.MountTargets[4].ManagedMCPCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileArisReportsReadyWhenBundleAndBridgeAreAvailable(t *testing.T) {
|
||||
result := Reconcile(Request{
|
||||
Config: Config{UsesAris: true},
|
||||
Aris: ArisInput{
|
||||
Available: true,
|
||||
BundleVersion: "test",
|
||||
LLMChatServerPath: "mcp-server.py",
|
||||
SkillCount: 2,
|
||||
BridgeAvailable: true,
|
||||
},
|
||||
})
|
||||
|
||||
if got := result.MountTargets[0].SyncState; got != "ready" {
|
||||
t.Fatalf("expected ready aris state, got %q", got)
|
||||
}
|
||||
if got := result.ArisBundleVersion; got != "test" {
|
||||
t.Fatalf("expected bundle version test, got %q", got)
|
||||
}
|
||||
}
|
||||
@ -92,22 +92,3 @@ func NotificationEnvelope(method string, params map[string]any) map[string]any {
|
||||
"seq": 0,
|
||||
}
|
||||
}
|
||||
|
||||
func ToolTextResult(id any, content string) map[string]any {
|
||||
result := map[string]any{
|
||||
"content": []map[string]any{
|
||||
{"type": "text", "text": content},
|
||||
},
|
||||
}
|
||||
return ResultEnvelope(id, result)
|
||||
}
|
||||
|
||||
func ToolErrorResult(id any, err error) map[string]any {
|
||||
result := map[string]any{
|
||||
"content": []map[string]any{
|
||||
{"type": "text", "text": fmt.Sprintf("Error: %v", err)},
|
||||
},
|
||||
"isError": true,
|
||||
}
|
||||
return ResultEnvelope(id, result)
|
||||
}
|
||||
|
||||
@ -342,30 +342,6 @@ func HandleChatTool(arguments map[string]any) (string, error) {
|
||||
return CallOpenAICompatible(baseURL, apiKey, model, messages)
|
||||
}
|
||||
|
||||
func HandleClaudeReviewTool(arguments map[string]any) (string, error) {
|
||||
prompt := strings.TrimSpace(StringArg(arguments, "prompt", ""))
|
||||
if prompt == "" {
|
||||
return "", errors.New("prompt is required")
|
||||
}
|
||||
model := strings.TrimSpace(
|
||||
StringArg(arguments, "model", EnvOrDefault("CLAUDE_REVIEW_MODEL", "")),
|
||||
)
|
||||
system := strings.TrimSpace(
|
||||
StringArg(arguments, "system", EnvOrDefault("CLAUDE_REVIEW_SYSTEM", "")),
|
||||
)
|
||||
tools := strings.TrimSpace(
|
||||
StringArg(arguments, "tools", EnvOrDefault("CLAUDE_REVIEW_TOOLS", "")),
|
||||
)
|
||||
timeout := IntArg(EnvOrDefault("CLAUDE_REVIEW_TIMEOUT_SEC", "600"), 600)
|
||||
return RunClaudeReview(
|
||||
prompt,
|
||||
model,
|
||||
system,
|
||||
tools,
|
||||
time.Duration(timeout)*time.Second,
|
||||
)
|
||||
}
|
||||
|
||||
func CallOpenAICompatible(
|
||||
baseURL,
|
||||
apiKey,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user