mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
149 lines
9.4 KiB
Go
149 lines
9.4 KiB
Go
//go:build linux
|
|
|
|
package scenario
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/nezhahq/nezha/integration/agentcompat/internal/client"
|
|
"github.com/nezhahq/nezha/integration/agentcompat/internal/dashboard"
|
|
)
|
|
|
|
func runMCPFilesystemOperations(ctx context.Context, assertions *AssertionSet, filesystem mcpFilesystemClient, permission permissionAgentContract, dashboardInstance *dashboard.Dashboard) error {
|
|
text := "typed filesystem payload"
|
|
textHash := sha256.Sum256([]byte(text))
|
|
textDigest := hex.EncodeToString(textHash[:])
|
|
written, err := filesystem.write(ctx, mcpFilesystemWrite{relative: "tree/text.txt", content: text, encoding: "utf8", mode: "0640", createDirs: true})
|
|
assertions.Record("fs.write create_dirs size and SHA", err == nil && written.StructuredContent.Size == int64(len(text)) && written.StructuredContent.SHA256 == textDigest, errorText(err))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tree, err := filesystem.list(ctx, "tree", false)
|
|
assertions.Record("fs.write applies exact file mode", err == nil && len(tree.StructuredContent.Entries) == 1 && tree.StructuredContent.Entries[0].Name == "text.txt" && tree.StructuredContent.Entries[0].Type == "file" && tree.StructuredContent.Entries[0].Size == int64(len(text)) && tree.StructuredContent.Entries[0].Mode == "0640", errorText(err))
|
|
_, err = filesystem.write(ctx, mcpFilesystemWrite{relative: ".hidden", content: "hidden", encoding: "utf8", mode: "0600"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
visible, err := filesystem.list(ctx, ".", false)
|
|
assertions.Record("fs.list hides dot entries with exact totals", err == nil && visible.StructuredContent.Total == 1 && entryNames(visible.StructuredContent.Entries) == "tree", errorText(err))
|
|
hidden, err := filesystem.list(ctx, ".", true)
|
|
assertions.Record("fs.list show_hidden includes exact dot entry", err == nil && hidden.StructuredContent.Total == 2 && entryNames(hidden.StructuredContent.Entries) == ".hidden,tree", errorText(err))
|
|
// Inventory and setup consume the PAT's fixed MCP call budget; rotate before content and CAS checks.
|
|
filesystem, err = refreshedMCPFilesystemClient(ctx, dashboardInstance, filesystem)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
readText, err := filesystem.read(ctx, "tree/text.txt", 0, int64(len(text)), "utf8")
|
|
assertions.Record("fs.read utf8 exact content size SHA and truncation", err == nil && readText.StructuredContent.Content == text && readText.StructuredContent.Encoding == "utf8" && readText.StructuredContent.Size == int64(len(text)) && readText.StructuredContent.SHA256 == textDigest && !readText.StructuredContent.Truncated, errorText(err))
|
|
binary := []byte{0x00, 0x41, 0xff, 0x42}
|
|
binaryHash := sha256.Sum256(binary)
|
|
binaryDigest := hex.EncodeToString(binaryHash[:])
|
|
_, err = filesystem.write(ctx, mcpFilesystemWrite{relative: "tree/binary.bin", content: base64.StdEncoding.EncodeToString(binary), encoding: "base64", mode: "0600"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
readBinary, err := filesystem.read(ctx, "tree/binary.bin", 0, int64(len(binary)), "base64")
|
|
assertions.Record("fs.read base64 exact content size and SHA", err == nil && readBinary.StructuredContent.Content == base64.StdEncoding.EncodeToString(binary) && readBinary.StructuredContent.Encoding == "base64" && readBinary.StructuredContent.Size == int64(len(binary)) && readBinary.StructuredContent.SHA256 == binaryDigest, errorText(err))
|
|
updated := "CAS updated payload"
|
|
updatedHash := sha256.Sum256([]byte(updated))
|
|
updatedDigest := hex.EncodeToString(updatedHash[:])
|
|
cas, err := filesystem.write(ctx, mcpFilesystemWrite{relative: "tree/text.txt", content: updated, encoding: "utf8", mode: "0640", ifMatchSHA256: textDigest})
|
|
assertions.Record("fs.write CAS success exact size and SHA", err == nil && cas.StructuredContent.Size == int64(len(updated)) && cas.StructuredContent.SHA256 == updatedDigest, errorText(err))
|
|
_, casErr := filesystem.write(ctx, mcpFilesystemWrite{relative: "tree/text.txt", content: "must-not-land", encoding: "utf8", mode: "0640", ifMatchSHA256: textDigest})
|
|
unchanged, readErr := filesystem.read(ctx, "tree/text.txt", 0, int64(len(updated)), "utf8")
|
|
assertions.Record("fs.write CAS mismatch is typed and leaves content unchanged", toolFailureContains(casErr, "if_match precondition failed") && readErr == nil && unchanged.StructuredContent.Content == updated && unchanged.StructuredContent.SHA256 == updatedDigest, errorText(errors.Join(casErr, readErr)))
|
|
permissionDirectory, pathErr := filesystem.root.Path("permission-denied")
|
|
if pathErr != nil {
|
|
return pathErr
|
|
}
|
|
if err := os.Mkdir(permissionDirectory.String(), 0o500); err != nil {
|
|
return err
|
|
}
|
|
permissionResponse, permissionErr := permission.filesystem.write(ctx, mcpFilesystemWrite{relative: "permission-denied/file.txt", content: "denied", encoding: "utf8", mode: "0600"})
|
|
permissionDeniedObserved := permissionErr == nil && permissionResponse.StructuredContent.Error == "permission denied"
|
|
assertions.Record("fs.write Agent filesystem permission denial is typed", permission.processContract && permissionDeniedObserved, fmt.Sprintf("%s; uid=%d gid=%d process_contract=%t", permissionResponse.StructuredContent.Error, permission.uid, permission.gid, permission.processContract))
|
|
if err := os.Remove(permissionDirectory.String()); err != nil {
|
|
return err
|
|
}
|
|
filesystem, err = refreshedMCPFilesystemClient(ctx, dashboardInstance, filesystem)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, missingErr := filesystem.read(ctx, "tree/missing.txt", 0, 1, "utf8")
|
|
assertions.Record("fs.read nonexistent path is typed", toolFailureContains(missingErr, "does not exist"), errorText(missingErr))
|
|
_, encodingErr := filesystem.read(ctx, "tree/text.txt", 0, 1, "rot13")
|
|
assertions.Record("fs.read invalid encoding is typed", toolFailureContains(encodingErr, "unknown encoding"), errorText(encodingErr))
|
|
_, writeEncodingErr := filesystem.write(ctx, mcpFilesystemWrite{relative: "tree/invalid-encoding.txt", content: "x", encoding: "rot13", mode: "0600"})
|
|
assertions.Record("fs.write invalid encoding is typed", toolFailureContains(writeEncodingErr, "unknown encoding"), errorText(writeEncodingErr))
|
|
_, modeErr := filesystem.write(ctx, mcpFilesystemWrite{relative: "tree/invalid-mode.txt", content: "x", encoding: "utf8", mode: "invalid"})
|
|
assertions.Record("fs.write invalid mode is typed", toolFailureContains(modeErr, "invalid mode"), errorText(modeErr))
|
|
filesystem, err = refreshedMCPFilesystemClient(ctx, dashboardInstance, filesystem)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oversize, oversizeErr := client.DoREST[agentcompatFsWriteContractRequest, agentcompatFsWriteContractResponse](ctx, filesystem.client, client.RESTRequest[agentcompatFsWriteContractRequest]{Method: http.MethodPost, Path: "/agentcompat/fs-write-contract", Body: &agentcompatFsWriteContractRequest{ServerID: filesystem.serverID, Operation: agentcompatFsWriteOperationOversize}})
|
|
oversizeHandlerObserved := oversize.AgentRPCResponse
|
|
productionMaxWriteCheck := oversizeErr == nil && oversize.Result.Error == "content exceeds max write size"
|
|
assertions.Record("fs.write Agent oversize contract is typed", oversizeHandlerObserved && productionMaxWriteCheck, fmt.Sprintf("%s; agent_handler=%t production_max_write_check=%t; agent_rpc_response=%t", oversize.Result.Error, oversizeHandlerObserved, productionMaxWriteCheck, oversize.AgentRPCResponse))
|
|
_, nonrecursiveErr := filesystem.delete(ctx, "tree", false)
|
|
assertions.Record("fs.delete nonrecursive rejects nonempty directory", toolFailureContains(nonrecursiveErr, "internal agent error"), errorText(nonrecursiveErr))
|
|
deleted, err := filesystem.delete(ctx, "tree", true)
|
|
assertions.Record("fs.delete recursive returns exact positive count", err == nil && deleted.StructuredContent.DeletedCount == 3, errorText(err))
|
|
final, err := filesystem.list(ctx, ".", true)
|
|
assertions.Record("fs.list final absence after recursive delete", err == nil && final.StructuredContent.Total == 1 && entryNames(final.StructuredContent.Entries) == ".hidden", errorText(err))
|
|
_, err = filesystem.delete(ctx, ".hidden", false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
empty, err := filesystem.list(ctx, ".", true)
|
|
assertions.Record("fs.list final fixture root is empty", err == nil && empty.StructuredContent.Total == 0 && len(empty.StructuredContent.Entries) == 0, errorText(err))
|
|
return err
|
|
}
|
|
|
|
type agentcompatFsWriteOperation string
|
|
|
|
const (
|
|
agentcompatFsWriteOperationOversize agentcompatFsWriteOperation = "oversize"
|
|
)
|
|
|
|
type agentcompatFsWriteContractRequest struct {
|
|
ServerID uint64 `json:"server_id"`
|
|
Operation agentcompatFsWriteOperation `json:"operation"`
|
|
}
|
|
|
|
type agentcompatFsWriteContractResponse struct {
|
|
Result client.FsWriteResult `json:"result"`
|
|
AgentRPCResponse bool `json:"agent_rpc_response"`
|
|
}
|
|
|
|
func refreshedMCPFilesystemClient(ctx context.Context, dashboardInstance *dashboard.Dashboard, filesystem mcpFilesystemClient) (mcpFilesystemClient, error) {
|
|
mcpClient, err := createScopedClient(ctx, dashboardInstance, []string{"nezha:*"})
|
|
if err != nil {
|
|
return mcpFilesystemClient{}, err
|
|
}
|
|
return newMCPFilesystemClient(mcpClient, filesystem.serverID, filesystem.root), nil
|
|
}
|
|
|
|
func entryNames(entries []client.FsEntry) string {
|
|
names := make([]string, 0, len(entries))
|
|
for _, entry := range entries {
|
|
names = append(names, entry.Name)
|
|
}
|
|
slices.Sort(names)
|
|
return strings.Join(names, ",")
|
|
}
|
|
|
|
func toolFailureContains(err error, text string) bool {
|
|
var failure *client.ToolFailure
|
|
return errors.As(err, &failure) && strings.Contains(failure.Message, text)
|
|
}
|