mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(studio): export plugins using supported JSON encodings
This commit is contained in:
1 parent
0195f8241a
commit
0f8dfa34f9
4 files changed
+316
-1
No files matched your search
@@ -15,6 +15,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
fileencoding "reasonix/internal/base/fileutil/encoding"
|
||||
"reasonix/internal/base/secrets"
|
||||
)
|
||||
|
||||
@@ -132,7 +133,7 @@ func credentialBearing(rel string) bool {
|
||||
// leaks it, while a false positive costs one variable to fill in.
|
||||
func StripCredentials(raw []byte) ([]byte, []string, error) {
|
||||
var doc any
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
if err := json.Unmarshal(fileencoding.DecodeToUTF8(raw), &doc); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
required := map[string]bool{}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package pluginpkg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
fileencoding "reasonix/internal/base/fileutil/encoding"
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestExportEncodedEndpointAndArgumentCredentials(t *testing.T) {
|
||||
for _, encoding := range []fileencoding.Kind{fileencoding.UTF8BOM, fileencoding.UTF16LE, fileencoding.UTF16BE, fileencoding.GB18030} {
|
||||
t.Run(fmt.Sprintf("encoding=%d", encoding), func(t *testing.T) {
|
||||
const manifest = `{"apiVersion":"reasonix.io/plugin/v2","name":"encoded","description":"中文插件","McpServers":{"remote":{"type":"http","URL":"https://fixture-user:[email protected]/mcp?tenant=demo&token=fixture-token","Headers":{"Authorization":"fixture-header"}},"process":{"command":"node","Args":["--password","fixture-argument","--mode","read"],"Env":{"KEY":"${EXISTING_KEY}"}}}}`
|
||||
raw, err := fileencoding.Encode(manifest, encoding)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := testenv.TempDir(t)
|
||||
file := filepath.Join(root, NativeManifest)
|
||||
if err := os.WriteFile(file, raw, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkg, _, err := ParseDir(root)
|
||||
if err != nil || pkg.Manifest.MCPServers["remote"].URL != "https://fixture-user:[email protected]/mcp?tenant=demo&token=fixture-token" || !slices.Equal(pkg.Manifest.MCPServers["process"].Args, []string{"--password", "fixture-argument", "--mode", "read"}) {
|
||||
t.Fatalf("operational manifest changed: %+v err=%v", pkg.Manifest, err)
|
||||
}
|
||||
archive, required, err := Export("encoded", root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := exportedEntries(t, archive)["encoded/"+NativeManifest]
|
||||
for _, secret := range []string{"fixture-user", "fixture-password", "fixture-token", "fixture-header", "fixture-argument"} {
|
||||
if strings.Contains(body, secret) {
|
||||
t.Fatalf("encoded export leaked %q: %s", secret, body)
|
||||
}
|
||||
}
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
servers := out["McpServers"].(map[string]any)
|
||||
remote := servers["remote"].(map[string]any)
|
||||
process := servers["process"].(map[string]any)
|
||||
if remote["URL"] != "https://example.test/mcp?tenant=demo&token=%3Credacted%3E" || process["Args"].([]any)[1] != "<redacted>" || process["Args"].([]any)[2] != "--mode" || process["Args"].([]any)[3] != "read" || out["description"] != "中文插件" {
|
||||
t.Fatalf("shared projection/metadata changed: %s", body)
|
||||
}
|
||||
if !slices.Equal(required, []string{"EXISTING_KEY", "REMOTE_AUTHORIZATION"}) {
|
||||
t.Fatalf("required variable identities changed: %v", required)
|
||||
}
|
||||
original, err := os.ReadFile(file)
|
||||
if err != nil || string(original) != string(raw) {
|
||||
t.Fatalf("source rewritten: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package pluginpkg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
fileencoding "reasonix/internal/base/fileutil/encoding"
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestExportUsesSupportedJSONEncodings(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
encoding fileencoding.Kind
|
||||
}{
|
||||
{"utf8", fileencoding.UTF8}, {"utf8-bom", fileencoding.UTF8BOM},
|
||||
{"utf16le", fileencoding.UTF16LE}, {"utf16be", fileencoding.UTF16BE},
|
||||
{"utf16le-no-bom", fileencoding.UTF16LENoBOM}, {"utf16be-no-bom", fileencoding.UTF16BENoBOM},
|
||||
{"gb18030", fileencoding.GB18030}, {"gbk", fileencoding.GBK},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
const manifest = `{"apiVersion":"reasonix.io/plugin/v2","name":"encoded","description":"中文插件","mcpServers":{"docs":{"command":"docs","env":{"TOKEN":"fixture-value"}}}}`
|
||||
raw, err := fileencoding.Encode(manifest, tc.encoding)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := testenv.TempDir(t)
|
||||
path := filepath.Join(root, NativeManifest)
|
||||
if err := os.WriteFile(path, raw, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkg, _, err := ParseDir(root)
|
||||
if err != nil || pkg.Manifest.Description != "中文插件" {
|
||||
t.Fatalf("installed parser: %+v, %v", pkg, err)
|
||||
}
|
||||
archive, required, err := Export("encoded", root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := exportedEntries(t, archive)["encoded/"+NativeManifest]
|
||||
var out struct {
|
||||
Description string `json:"description"`
|
||||
MCPServers map[string]struct {
|
||||
Env map[string]string `json:"env"`
|
||||
} `json:"mcpServers"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out.Description != "中文插件" || out.MCPServers["docs"].Env["TOKEN"] != "${DOCS_TOKEN}" || strings.Join(required, ",") != "DOCS_TOKEN" {
|
||||
t.Fatalf("export=%s required=%v", body, required)
|
||||
}
|
||||
original, err := os.ReadFile(path)
|
||||
if err != nil || string(original) != string(raw) {
|
||||
t.Fatalf("source rewritten: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportMixedCaseCredentialFields(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, servers, env, headers string
|
||||
encoding fileencoding.Kind
|
||||
}{
|
||||
{"utf16le-title", "McpServers", "Env", "Headers", fileencoding.UTF16LE},
|
||||
{"utf16le-upper", "MCPSERVERS", "ENV", "HEADERS", fileencoding.UTF16LE},
|
||||
{"utf16le-fold", "mcpServerſ", "eNv", "headerſ", fileencoding.UTF16LE},
|
||||
{"gb18030-mixed", "mCpSeRvErS", "eNv", "hEaDeRs", fileencoding.GB18030},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
manifest := fmt.Sprintf(`{"apiVersion":"reasonix.io/plugin/v2","name":"encoded","description":"中文插件",%q:{"docs":{"command":"docs",%q:{"TOKEN":"fixture-token"}},"wiki":{"type":"http","url":"https://wiki.example",%q:{"Authorization":"fixture-header"}}},"runtime":{"command":"fixture-sidecar",%q:{"KEY":"fixture-runtime"}}}`, tc.servers, tc.env, tc.headers, tc.env)
|
||||
raw, err := fileencoding.Encode(manifest, tc.encoding)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := testenv.TempDir(t)
|
||||
file := filepath.Join(root, NativeManifest)
|
||||
if err := os.WriteFile(file, raw, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkg, _, err := ParseDir(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pkg.Manifest.MCPServers["docs"].Env["TOKEN"] != "fixture-token" || pkg.Manifest.MCPServers["wiki"].Headers["Authorization"] != "fixture-header" || pkg.Manifest.Runtime == nil || pkg.Manifest.Runtime.Env["KEY"] != "fixture-runtime" {
|
||||
t.Fatalf("accepted fields not decoded: %+v", pkg.Manifest)
|
||||
}
|
||||
archive, required, err := Export("encoded", root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := exportedEntries(t, archive)["encoded/"+NativeManifest]
|
||||
if strings.Join(required, ",") != "DOCS_TOKEN,KEY,WIKI_AUTHORIZATION" || strings.Contains(body, "fixture-token") || strings.Contains(body, "fixture-header") || strings.Contains(body, "fixture-runtime") {
|
||||
t.Fatalf("exported body=%s, required=%v", body, required)
|
||||
}
|
||||
var doc map[string]any
|
||||
if err := json.Unmarshal([]byte(body), &doc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
servers := doc[tc.servers].(map[string]any)
|
||||
if servers["docs"].(map[string]any)[tc.env].(map[string]any)["TOKEN"] != "${DOCS_TOKEN}" || servers["wiki"].(map[string]any)[tc.headers].(map[string]any)["Authorization"] != "${WIKI_AUTHORIZATION}" || doc["runtime"].(map[string]any)[tc.env].(map[string]any)["KEY"] != "${KEY}" || doc["description"] != "中文插件" {
|
||||
t.Fatalf("field spelling/metadata/value changed: %s", body)
|
||||
}
|
||||
original, err := os.ReadFile(file)
|
||||
if err != nil || string(original) != string(raw) {
|
||||
t.Fatalf("source rewritten: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
fileencoding "reasonix/internal/base/fileutil/encoding"
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/ext/pluginpkg"
|
||||
)
|
||||
|
||||
func TestPluginExportRoundTripsSupportedJSONEncodings(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, body string
|
||||
encoding fileencoding.Kind
|
||||
}{
|
||||
{"gb18030", `{"mcpServers":{"docs":{"command":"docs","env":{"TOKEN":"fixture-token"}},"wiki":{"type":"http","url":"https://wiki.example","title":"中文文档","headers":{"Authorization":"fixture-header"}}}}`, fileencoding.GB18030},
|
||||
{"utf16le-mixed-case", `{"McpServers":{"docs":{"command":"docs","Env":{"TOKEN":"fixture-token"}},"wiki":{"type":"http","url":"https://wiki.example","title":"中文文档","Headers":{"Authorization":"fixture-header"}}}}`, fileencoding.UTF16LE},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) { checkPluginExportEncodingRoundTrip(t, tc.body, tc.encoding) })
|
||||
}
|
||||
}
|
||||
|
||||
func checkPluginExportEncodingRoundTrip(t *testing.T, mcpBody string, mcpEncoding fileencoding.Kind) {
|
||||
t.Helper()
|
||||
home, _, base := pluginHome(t)
|
||||
source := testenv.TempDir(t)
|
||||
files := map[string][]byte{}
|
||||
for _, tc := range []struct {
|
||||
path, body string
|
||||
encoding fileencoding.Kind
|
||||
}{
|
||||
{pluginpkg.ClaudeManifest, `{"name":"encoded-transport","description":"中文插件"}`, fileencoding.UTF16LE},
|
||||
{".mcp.json", mcpBody, mcpEncoding},
|
||||
{"fixture.json", `{"env":{"KEEP":"中文内容"}}`, fileencoding.UTF8BOM},
|
||||
} {
|
||||
raw, err := fileencoding.Encode(tc.body, tc.encoding)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writePluginFile(t, filepath.Join(source, tc.path), string(raw))
|
||||
files[tc.path] = raw
|
||||
}
|
||||
install := func(path string) {
|
||||
t.Helper()
|
||||
resp := postJSON(t, base+"/plugins/plan", map[string]any{"source": path})
|
||||
plan := decodeInstallSource(t, resp)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("plan status=%d: %v", resp.StatusCode, plan)
|
||||
}
|
||||
resp = postJSON(t, base+"/plugins/install", map[string]any{"source": path, "planId": plan["planId"]})
|
||||
result := decodeInstallSource(t, resp)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK || result["status"] != "done" {
|
||||
t.Fatalf("install status=%d: %v", resp.StatusCode, result)
|
||||
}
|
||||
}
|
||||
install(source)
|
||||
plugins := getPlugins(t, base)
|
||||
if len(plugins) != 1 || plugins[0].Description != "中文插件" || len(plugins[0].MCPServers) != 2 {
|
||||
t.Fatalf("installed plugins=%+v", plugins)
|
||||
}
|
||||
resp, err := http.Get(base + "/plugins/encoded-transport/export")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
archive, err := io.ReadAll(resp.Body)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("export status=%d body=%s error=%v", resp.StatusCode, archive, err)
|
||||
}
|
||||
if got := resp.Header.Get("X-Reasonix-Required-Env"); got != "DOCS_TOKEN,WIKI_AUTHORIZATION" {
|
||||
t.Fatalf("required env=%q", got)
|
||||
}
|
||||
zr, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries := map[string][]byte{}
|
||||
for _, f := range zr.File {
|
||||
r, err := f.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := io.ReadAll(r)
|
||||
r.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries[f.Name] = data
|
||||
}
|
||||
manifest := entries["encoded-transport/"+pluginpkg.ClaudeManifest]
|
||||
mcp := entries["encoded-transport/.mcp.json"]
|
||||
if !json.Valid(manifest) || !bytes.Contains(manifest, []byte("中文插件")) ||
|
||||
!json.Valid(mcp) || !bytes.Contains(mcp, []byte("中文文档")) || !bytes.Contains(mcp, []byte("${DOCS_TOKEN}")) || !bytes.Contains(mcp, []byte("${WIKI_AUTHORIZATION}")) || bytes.Contains(mcp, []byte("fixture-token")) || bytes.Contains(mcp, []byte("fixture-header")) {
|
||||
t.Fatalf("exported manifest=%s mcp=%s", manifest, mcp)
|
||||
}
|
||||
if !bytes.Equal(entries["encoded-transport/fixture.json"], files["fixture.json"]) {
|
||||
t.Fatal("ordinary content was rewritten")
|
||||
}
|
||||
for path, raw := range files {
|
||||
for _, root := range []string{source, pluginpkg.InstallRoot(home, "encoded-transport")} {
|
||||
original, err := os.ReadFile(filepath.Join(root, path))
|
||||
if err != nil || !bytes.Equal(original, raw) {
|
||||
t.Fatalf("source file %s was rewritten: %v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodDelete, base+"/plugins/encoded-transport", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deleted, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deleted.Body.Close()
|
||||
if deleted.StatusCode != http.StatusOK {
|
||||
t.Fatalf("remove status=%d", deleted.StatusCode)
|
||||
}
|
||||
archivePath := filepath.Join(testenv.TempDir(t), "encoded.zip")
|
||||
if err := os.WriteFile(archivePath, archive, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
install(archivePath)
|
||||
plugins = getPlugins(t, base)
|
||||
if len(plugins) != 1 || plugins[0].Description != "中文插件" || len(plugins[0].MCPServers) != 2 {
|
||||
t.Fatalf("reinstalled plugins=%+v", plugins)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user