mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50:12 +00:00
236 lines
11 KiB
Go
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
|
|
}
|