Files
nezha_domains/integration/agentcompat/internal/scenario/mcp_filesystem.go
T
2026-07-20 04:51:20 +00:00

236 lines
11 KiB
Go

//go:build linux
package scenario
import (
"context"
"errors"
"fmt"
"os"
"slices"
"syscall"
"github.com/nezhahq/nezha/integration/agentcompat/internal/agent"
"github.com/nezhahq/nezha/integration/agentcompat/internal/client"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
"github.com/nezhahq/nezha/integration/agentcompat/internal/dashboard"
"github.com/nezhahq/nezha/integration/agentcompat/internal/evidence"
"github.com/nezhahq/nezha/integration/agentcompat/internal/fixture"
)
const mcpFilesystemScenarioName = "mcp-filesystem"
type MCPFilesystemInput struct {
Paths contract.Paths
}
type MCPFilesystem struct{}
type mcpFilesystemClient struct {
client *client.Client
serverID uint64
root fixture.AgentRoot
}
type mcpFilesystemWrite struct {
relative string
content string
encoding string
mode string
ifMatchSHA256 string
createDirs bool
}
func newMCPFilesystemClient(mcpClient *client.Client, serverID uint64, root fixture.AgentRoot) mcpFilesystemClient {
return mcpFilesystemClient{client: mcpClient, serverID: serverID, root: root}
}
func (filesystem mcpFilesystemClient) list(ctx context.Context, relative string, showHidden bool) (client.ToolCallResult[client.FsListResult], error) {
path, err := filesystem.root.Path(relative)
if err != nil {
return client.ToolCallResult[client.FsListResult]{}, err
}
arguments := client.FsListArguments{ServerID: filesystem.serverID, Path: path.String(), ShowHidden: showHidden}
return client.CallTool[client.FsListArguments, client.FsListResult](ctx, filesystem.client, client.ToolCall[client.FsListArguments]{Name: "fs.list", Arguments: arguments})
}
func (filesystem mcpFilesystemClient) read(ctx context.Context, relative string, offset, length int64, encoding string) (client.ToolCallResult[client.FsReadResult], error) {
path, err := filesystem.root.Path(relative)
if err != nil {
return client.ToolCallResult[client.FsReadResult]{}, err
}
arguments := client.FsReadArguments{ServerID: filesystem.serverID, Path: path.String(), Offset: offset, Length: length, Encoding: encoding}
return client.CallTool[client.FsReadArguments, client.FsReadResult](ctx, filesystem.client, client.ToolCall[client.FsReadArguments]{Name: "fs.read", Arguments: arguments})
}
func (filesystem mcpFilesystemClient) write(ctx context.Context, write mcpFilesystemWrite) (client.ToolCallResult[client.FsWriteResult], error) {
path, err := filesystem.root.Path(write.relative)
if err != nil {
return client.ToolCallResult[client.FsWriteResult]{}, err
}
arguments := client.FsWriteArguments{ServerID: filesystem.serverID, Path: path.String(), Content: write.content, Encoding: write.encoding, Mode: write.mode, IfMatchSHA256: write.ifMatchSHA256, CreateDirs: write.createDirs}
return client.CallTool[client.FsWriteArguments, client.FsWriteResult](ctx, filesystem.client, client.ToolCall[client.FsWriteArguments]{Name: "fs.write", Arguments: arguments})
}
func (filesystem mcpFilesystemClient) delete(ctx context.Context, relative string, recursive bool) (client.ToolCallResult[client.FsDeleteResult], error) {
path, err := filesystem.root.DestructivePath(relative)
if err != nil {
return client.ToolCallResult[client.FsDeleteResult]{}, err
}
arguments := client.FsDeleteArguments{ServerID: filesystem.serverID, Path: path.String(), Recursive: recursive}
return client.CallTool[client.FsDeleteArguments, client.FsDeleteResult](ctx, filesystem.client, client.ToolCall[client.FsDeleteArguments]{Name: "fs.delete", Arguments: arguments})
}
func (MCPFilesystem) Run(ctx context.Context, input MCPFilesystemInput) (result Result, runErr error) {
assertions := NewAssertionSet()
fixtureParent, err := os.MkdirTemp("", "agentcompat-mcp-filesystem-")
if err != nil {
return mcpFilesystemFinish(assertions, err)
}
root, err := fixture.NewAgentRoot(fixtureParent, "agent-filesystem")
if err != nil {
_ = os.RemoveAll(fixtureParent)
return mcpFilesystemFinish(assertions, err)
}
dashboardInstance, err := dashboard.Start(ctx, dashboard.StartConfig{SourceDir: input.Paths.NezhaSource().String(), ReceiptGate: true})
if err != nil {
_ = os.RemoveAll(fixtureParent)
return mcpFilesystemFinish(assertions, err)
}
defer func() {
cleanupErr := errors.Join(dashboardInstance.Stop(context.Background()), os.RemoveAll(fixtureParent))
result.CleanupOK = cleanupErr == nil && dashboardInstance.CleanupReceipt().Passed
if cleanupErr != nil && runErr == nil {
runErr = cleanupErr
result.Passed = false
result.Error = errorText(cleanupErr)
}
}()
agentInstance, err := agent.Start(ctx, agent.AgentStartConfig{SourceDir: input.Paths.AgentSource().String(), Endpoint: dashboardInstance.Endpoint(), Secret: dashboardInstance.AgentSecret(), UUID: "00000000-0000-0000-0000-000000000212"})
if err != nil {
return mcpFilesystemFinish(assertions, err)
}
defer func() {
cleanupErr := agentInstance.Stop(context.Background())
if cleanupErr != nil && runErr == nil {
runErr = cleanupErr
result.Passed = false
result.Error = errorText(cleanupErr)
}
}()
if err := dashboardInstance.WaitForReceiptAccepted(ctx); err != nil {
return mcpFilesystemFinish(assertions, err)
}
if err := dashboardInstance.ReleaseReceipt(ctx); err != nil {
return mcpFilesystemFinish(assertions, err)
}
readiness, err := agentInstance.WaitReady(ctx, dashboardInstance)
if err != nil {
return mcpFilesystemFinish(assertions, err)
}
mcpClient, err := createScopedClient(ctx, dashboardInstance, []string{"nezha:*"})
if err != nil {
return mcpFilesystemFinish(assertions, err)
}
initialize, err := mcpClient.Initialize(ctx)
assertions.Record("MCP initialize exact protocol and server", err == nil && initialize.ProtocolVersion == "2024-11-05" && initialize.ServerInfo.Name == "nezha-mcp" && initialize.ServerInfo.Version != "", errorText(err))
tools, err := mcpClient.ListTools(ctx)
toolNames := make([]string, 0, len(tools.Tools))
for _, tool := range tools.Tools {
toolNames = append(toolNames, tool.Name)
}
wantTools := []string{"fs.delete", "fs.list", "fs.read", "fs.write", "meta.whoami", "server.get", "server.list"}
assertions.Record("tools.list exposes filesystem identity and inventory tools", err == nil && containsAll(toolNames, wantTools), errorText(err))
whoami, err := client.CallTool[struct{}, client.WhoAmIResult](ctx, mcpClient, client.ToolCall[struct{}]{Name: "meta.whoami", Arguments: struct{}{}})
identity := whoami.StructuredContent
assertions.Record("meta.whoami exact administrator PAT identity", err == nil && identity.UserID != 0 && identity.IsAdmin && identity.TokenID != 0 && identity.TokenName == "agentcompat-scope-check" && slices.Equal(identity.Scopes, []string{"nezha:*"}) && len(identity.ServerIDs) == 0, errorText(err))
servers, err := client.CallTool[client.ServerListArguments, client.ServerListResult](ctx, mcpClient, client.ToolCall[client.ServerListArguments]{Name: "server.list", Arguments: client.ServerListArguments{OnlineOnly: true}})
serverID := uint64(0)
for _, server := range servers.StructuredContent.Servers {
if server.UUID == agentInstance.UUID() && server.Online {
serverID = server.ID
}
}
assertions.Record("server.list exact online UUID and count", err == nil && readiness.UUID == agentInstance.UUID() && serverID != 0 && servers.StructuredContent.Count == len(servers.StructuredContent.Servers), errorText(err))
server, err := client.CallTool[client.ServerGetArguments, client.ServerGetResult](ctx, mcpClient, client.ToolCall[client.ServerGetArguments]{Name: "server.get", Arguments: client.ServerGetArguments{ServerID: serverID}})
assertions.Record("server.get exact typed identity Host and State", err == nil && server.StructuredContent.ID == serverID && server.StructuredContent.UUID == agentInstance.UUID() && string(server.StructuredContent.Host) != "null" && string(server.StructuredContent.State) != "null", errorText(err))
if err != nil || serverID == 0 {
return mcpFilesystemFinish(assertions, errors.New("filesystem scenario inventory setup failed"))
}
filesystem := newMCPFilesystemClient(mcpClient, serverID, root)
if err := verifyMCPFilesystemPathGuards(ctx, assertions, filesystem, fixtureParent); err != nil {
return mcpFilesystemFinish(assertions, err)
}
const agentFixtureIdentity = 65534
permissionCredential := &syscall.Credential{Uid: agentFixtureIdentity, Gid: agentFixtureIdentity}
permissionAgent, err := agent.Start(ctx, agent.AgentStartConfig{SourceDir: input.Paths.AgentSource().String(), Endpoint: dashboardInstance.Endpoint(), Secret: dashboardInstance.AgentSecret(), UUID: "00000000-0000-0000-0000-000000000213", Credential: permissionCredential})
if err != nil {
return mcpFilesystemFinish(assertions, err)
}
defer func() {
cleanupErr := permissionAgent.Stop(context.Background())
if cleanupErr != nil && runErr == nil {
runErr = cleanupErr
result.Passed = false
result.Error = errorText(cleanupErr)
}
}()
if _, err := permissionAgent.WaitReady(ctx, dashboardInstance); err != nil {
return mcpFilesystemFinish(assertions, err)
}
permissionClient, err := createScopedClient(ctx, dashboardInstance, []string{"nezha:*"})
if err != nil {
return mcpFilesystemFinish(assertions, err)
}
permissionServers, err := client.CallTool[client.ServerListArguments, client.ServerListResult](ctx, permissionClient, client.ToolCall[client.ServerListArguments]{Name: "server.list", Arguments: client.ServerListArguments{OnlineOnly: true}})
if err != nil {
return mcpFilesystemFinish(assertions, err)
}
permissionServerID := uint64(0)
for _, server := range permissionServers.StructuredContent.Servers {
if server.UUID == permissionAgent.UUID() && server.Online {
permissionServerID = server.ID
}
}
if permissionServerID == 0 {
return mcpFilesystemFinish(assertions, errors.New("permission Agent is absent from server.list"))
}
permissionContract, err := observePermissionAgent(permissionAgent, permissionCredential, newMCPFilesystemClient(permissionClient, permissionServerID, root))
if err != nil {
return mcpFilesystemFinish(assertions, err)
}
if err := runMCPFilesystemOperations(ctx, assertions, filesystem, permissionContract, dashboardInstance); err != nil {
return mcpFilesystemFinish(assertions, err)
}
return mcpFilesystemFinish(assertions, nil)
}
func containsAll(values, required []string) bool {
for _, value := range required {
if !slices.Contains(values, value) {
return false
}
}
return true
}
func mcpFilesystemFinish(assertions *AssertionSet, runErr error) (Result, error) {
failedAssertion := false
for _, assertion := range assertions.assertions {
if !assertion.Passed {
failedAssertion = true
if runErr == nil {
runErr = fmt.Errorf("%s: %s", assertion.Name, assertion.Details)
}
}
}
if runErr != nil && !failedAssertion {
assertions.Record("scenario execution completed", false, errorText(runErr))
}
result := Result{Name: mcpFilesystemScenarioName, Passed: runErr == nil, Assertions: assertions.Results()}
if runErr != nil {
result.Error = evidence.Redact(runErr.Error())
}
return result, runErr
}