From 3d2dd19001fe135538b6afc34548eb1a876fcdaa Mon Sep 17 00:00:00 2001 From: J3vb <192430104+J3vb@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:10:02 +0200 Subject: [PATCH] feat(plugin): enforce manifest-declared per-command ACL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes audit-2026-04-07 CRITICAL #3. Holding the `commands` capability used to bind whatever names the guest module returned from `list_commands`, so an admin enabling a plugin could not know which commands it would claim and a plugin could widen its own command surface after review. The manifest is now the authority. `plugin.json` gains a `commands` block (`[{"name": "hello"}]`) and `RegisterCommand` refuses any undeclared name — the single choke point both auto-registration and direct registration route through, so no caller can bypass it. Declared names are validated to the dispatcher's canonical lowercase form, deduplicated, and capped at 64. The object shape matches docs/plans/slash-commands.md so the richer per-command schema can land later without a manifest migration. Also pins the two neighbouring CRITICALs that verification found already closed, and adds the storage key cap host_storage.go's doc comment already promised: - #2 (storage key isolation): TestStorageKeysIsolatedPerPlugin — the KV namespace is the caller's Instance.ID with no parameter to override it, and plugin_kv PRIMARY KEY (plugin_id, key) makes the split structural. - #4 (event rate limit): TestEventDeliveryHasNoGuestPath — EventSink.Dispatch invokes no guest code and has no callers, so there is nothing to limit yet; a SECURITY GATE comment requires the limiter in whatever change wires delivery. - #5 mitigation: TestEmptyAllowlistDeniesEveryHost — the shipped empty http_allowlist must fail closed. BREAKING CHANGE: a plugin declaring the `commands` capability must now list its commands in the manifest's `commands` block; undeclared names no longer bind. Only the in-repo `hello` example is affected and is updated here. --- Server/plugin/audit_closure_test.go | 207 +++++++++++++++++++++++ Server/plugin/errors.go | 6 + Server/plugin/examples/hello/README.md | 5 + Server/plugin/examples/hello/plugin.json | 3 + Server/plugin/host_commands.go | 8 + Server/plugin/host_events.go | 18 +- Server/plugin/host_storage.go | 13 ++ Server/plugin/manifest.go | 77 ++++++++- Server/plugin/plugin_test.go | 6 +- Server/plugin/sandbox_wazero.go | 5 +- Server/plugin/sandbox_wazero_test.go | 10 +- 11 files changed, 339 insertions(+), 19 deletions(-) create mode 100644 Server/plugin/audit_closure_test.go diff --git a/Server/plugin/audit_closure_test.go b/Server/plugin/audit_closure_test.go new file mode 100644 index 00000000..744b15c1 --- /dev/null +++ b/Server/plugin/audit_closure_test.go @@ -0,0 +1,207 @@ +// Audit 2026-04-07 plugin CRITICAL closure invariants. +// +// Each test here pins a property the closure table in +// docs/audit-2026-04-07.md cites as the reason a CRITICAL is closed. They live +// in the default-build file set on purpose so they run on every +// `go test ./...`, not only under -tags wazero. + +package plugin + +import ( + "context" + "errors" + "strings" + "testing" +) + +// TestRegisterCommandRequiresManifestDeclaration locks finding #3 (per-command +// ACL). Holding the `commands` capability is not enough: only names the +// manifest's `commands` block declares may bind, so a guest module cannot +// widen its own command surface via list_commands. +func TestRegisterCommandRequiresManifestDeclaration(t *testing.T) { + mem := openPluginTestDB(t) + reg, err := NewRegistry(Config{Directory: t.TempDir(), Store: mem}) + if err != nil { + t.Fatalf("NewRegistry: %v", err) + } + t.Cleanup(func() { _ = reg.Close(context.Background()) }) + + manifest, err := ParseManifest([]byte( + `{"name":"claimer","version":"0.1.0","entrypoint":"p.wasm","permissions":["commands"],"commands":[{"name":"declared"}]}`)) + if err != nil { + t.Fatalf("ParseManifest: %v", err) + } + inst := &Instance{ID: 1, Manifest: manifest} + + if err := reg.RegisterCommand("declared", inst); err != nil { + t.Fatalf("declared command must bind: %v", err) + } + // "/DECLARED" normalizes to the declared name and must still bind. + if err := reg.RegisterCommand("/DECLARED", inst); err != nil { + t.Fatalf("normalized declared command must bind: %v", err) + } + for _, undeclared := range []string{"undeclared", "ban", "declared2"} { + if err := reg.RegisterCommand(undeclared, inst); !errors.Is(err, ErrCommandNotDeclared) { + t.Fatalf("RegisterCommand(%q) = %v, want ErrCommandNotDeclared", undeclared, err) + } + reg.mu.RLock() + _, bound := reg.commands[undeclared] + reg.mu.RUnlock() + if bound { + t.Fatalf("undeclared command %q must not be bound", undeclared) + } + } +} + +func TestManifestCommandsValidation(t *testing.T) { + bad := map[string]string{ + "no commands capability": `{"name":"x","version":"1","entrypoint":"x.wasm","commands":[{"name":"a"}]}`, + "uppercase name": `{"name":"x","version":"1","entrypoint":"x.wasm","permissions":["commands"],"commands":[{"name":"Ban"}]}`, + "leading slash": `{"name":"x","version":"1","entrypoint":"x.wasm","permissions":["commands"],"commands":[{"name":"/ban"}]}`, + "empty name": `{"name":"x","version":"1","entrypoint":"x.wasm","permissions":["commands"],"commands":[{"name":""}]}`, + "whitespace in name": `{"name":"x","version":"1","entrypoint":"x.wasm","permissions":["commands"],"commands":[{"name":"a b"}]}`, + "duplicate name": `{"name":"x","version":"1","entrypoint":"x.wasm","permissions":["commands"],"commands":[{"name":"a"},{"name":"a"}]}`, + } + for label, body := range bad { + t.Run(label, func(t *testing.T) { + if _, err := ParseManifest([]byte(body)); err == nil { + t.Fatalf("expected rejection for %s", label) + } + }) + } + + // Extra per-command fields from the richer schema in + // docs/plans/slash-commands.md must still parse (forward compatibility). + m, err := ParseManifest([]byte( + `{"name":"x","version":"1","entrypoint":"x.wasm","permissions":["commands"],"commands":[{"name":"kick","description":"d","options":[]}]}`)) + if err != nil { + t.Fatalf("rich command spec must parse: %v", err) + } + if !m.DeclaresCommand("kick") || m.DeclaresCommand("ban") { + t.Fatalf("DeclaresCommand wrong: %+v", m.Commands) + } +} + +// TestStorageKeysIsolatedPerPlugin locks finding #2 (per-plugin key +// isolation). The namespace is the caller's Instance.ID, which no plugin- +// supplied input can influence, and plugin_kv's PRIMARY KEY (plugin_id, key) +// keeps identical keys from two plugins in separate rows. +func TestStorageKeysIsolatedPerPlugin(t *testing.T) { + ctx := context.Background() + mem := openPluginTestDB(t) + reg, err := NewRegistry(Config{Store: mem}) + if err != nil { + t.Fatal(err) + } + newInst := func(name string) *Instance { + id, instErr := mem.InstallPlugin(ctx, name, "0.1", "{}") + if instErr != nil { + t.Fatalf("InstallPlugin(%s): %v", name, instErr) + } + return &Instance{ + ID: id, + Manifest: &Manifest{Name: name, Permissions: []string{string(CapStorage)}}, + } + } + alice, mallory := newInst("alice"), newInst("mallory") + + if err := reg.StoragePut(ctx, alice, "secret", []byte("alice-value")); err != nil { + t.Fatalf("StoragePut alice: %v", err) + } + // Same key, different plugin: the writes must not collide. + if err := reg.StoragePut(ctx, mallory, "secret", []byte("mallory-value")); err != nil { + t.Fatalf("StoragePut mallory: %v", err) + } + got, err := reg.StorageGet(ctx, alice, "secret") + if err != nil || string(got) != "alice-value" { + t.Fatalf("alice's value was clobbered: got %q err %v", got, err) + } + + // Scan is namespaced too: an empty prefix returns only the caller's keys. + all, err := reg.StorageScan(ctx, mallory, "", 0) + if err != nil { + t.Fatalf("StorageScan: %v", err) + } + if len(all) != 1 || string(all["secret"]) != "mallory-value" { + t.Fatalf("scan leaked across plugin namespaces: %v", all) + } + + // Deleting from one namespace leaves the other intact. + if err := reg.StorageDelete(ctx, mallory, "secret"); err != nil { + t.Fatalf("StorageDelete: %v", err) + } + if got, err := reg.StorageGet(ctx, alice, "secret"); err != nil || string(got) != "alice-value" { + t.Fatalf("alice's key was deleted by mallory: got %q err %v", got, err) + } +} + +func TestStorageRejectsOversizedKeyAndValue(t *testing.T) { + ctx := context.Background() + mem := openPluginTestDB(t) + reg, err := NewRegistry(Config{Store: mem}) + if err != nil { + t.Fatal(err) + } + id, err := mem.InstallPlugin(ctx, "bloat", "0.1", "{}") + if err != nil { + t.Fatal(err) + } + inst := &Instance{ID: id, Manifest: &Manifest{Name: "bloat", Permissions: []string{string(CapStorage)}}} + + if err := reg.StoragePut(ctx, inst, "", []byte("v")); err == nil { + t.Fatal("empty key must be rejected") + } + if err := reg.StoragePut(ctx, inst, strings.Repeat("k", maxPluginKeyBytes+1), []byte("v")); err == nil { + t.Fatal("oversized key must be rejected") + } + if err := reg.StoragePut(ctx, inst, "k", make([]byte, maxPluginValueBytes+1)); err == nil { + t.Fatal("oversized value must be rejected") + } + if err := reg.StoragePut(ctx, inst, strings.Repeat("k", maxPluginKeyBytes), make([]byte, maxPluginValueBytes)); err != nil { + t.Fatalf("at-limit key/value must be accepted: %v", err) + } +} + +// TestEventDeliveryHasNoGuestPath locks finding #4. The finding (a plugin +// slowing the server by handling events slowly) is not reachable today: +// Subscribe requires the capability and Dispatch invokes no guest code in +// either build. If this test has to change because Dispatch grew a real +// delivery path, that change must also bring the per-plugin rate limit — see +// the SECURITY GATE comment on EventSink.Dispatch. +func TestEventDeliveryHasNoGuestPath(t *testing.T) { + sink := NewEventSink() + noCap := &Instance{ID: 1, Manifest: &Manifest{Name: "nocap"}} + if err := sink.Subscribe("message_send", noCap); !errors.Is(err, ErrCapabilityNotGranted) { + t.Fatalf("Subscribe without the events capability = %v, want ErrCapabilityNotGranted", err) + } + + sub := &Instance{ID: 2, Manifest: &Manifest{Name: "sub", Permissions: []string{string(CapEvents)}}} + if err := sink.Subscribe("message_send", sub); err != nil { + t.Fatalf("Subscribe: %v", err) + } + // inst.module is nil here, so a real guest call would panic or error. + sink.Dispatch(context.Background(), "message_send", []byte(`{}`)) + + sink.UnsubscribeAll(sub) + sink.mu.Lock() + remaining := len(sink.subs) + sink.mu.Unlock() + if remaining != 0 { + t.Fatalf("UnsubscribeAll left %d topics behind", remaining) + } +} + +// TestEmptyAllowlistDeniesEveryHost locks the standing mitigation for finding +// #5 (HTTP exfiltration to allowlisted hosts): the shipped default +// `plugins.http_allowlist` is empty (config.DefaultConfig), and an empty +// allowlist must deny every destination rather than fall open. +func TestEmptyAllowlistDeniesEveryHost(t *testing.T) { + for _, allowlist := range [][]string{nil, {}, {"", " "}} { + reg := newTestRegistry(allowlist) + for _, host := range []string{"example.com", "api.github.com", "attacker.test", "localhost"} { + if reg.hostAllowed(host) { + t.Fatalf("allowlist %v must not permit %q", allowlist, host) + } + } + } +} diff --git a/Server/plugin/errors.go b/Server/plugin/errors.go index fbec2ba3..2be9ae36 100644 --- a/Server/plugin/errors.go +++ b/Server/plugin/errors.go @@ -14,3 +14,9 @@ var ErrPluginNotFound = errors.New("plugin not found") // ErrCapabilityNotGranted is returned when a host API call would require a // capability the plugin's manifest did not declare. var ErrCapabilityNotGranted = errors.New("plugin capability not granted") + +// ErrCommandNotDeclared is returned when a plugin tries to bind a slash +// command its manifest did not list in `commands`. The manifest — not the +// guest module — is the authority on which commands a plugin may own, so an +// admin can see the full command surface before enabling the plugin. +var ErrCommandNotDeclared = errors.New("plugin command not declared in manifest") diff --git a/Server/plugin/examples/hello/README.md b/Server/plugin/examples/hello/README.md index 4769f792..68b8ead0 100644 --- a/Server/plugin/examples/hello/README.md +++ b/Server/plugin/examples/hello/README.md @@ -8,6 +8,11 @@ Phase C Step 9 — proof-of-life plugin used by `Server/plugin/plugin_test.go`. The manifest is the only file the default (no-`-tags wazero`) build needs — the registry persists it into the plugins table without executing the .wasm. +The `commands` block is the per-command ACL: activation binds only the names +listed there, so `list_commands` returning anything else is ignored. Keep +`plugin.json`'s list and `listCommandsJSON` in `main.go` in sync — a name in +the WASM but not the manifest simply never binds. + ## Building the WASM `main.go` in this directory implements the full plugin ABI diff --git a/Server/plugin/examples/hello/plugin.json b/Server/plugin/examples/hello/plugin.json index 7ed04242..1ea9c5fa 100644 --- a/Server/plugin/examples/hello/plugin.json +++ b/Server/plugin/examples/hello/plugin.json @@ -5,6 +5,9 @@ "description": "Trivial proof-of-life plugin: registers /hello and echoes message_send events.", "entrypoint": "hello.wasm", "permissions": ["commands", "events", "storage"], + "commands": [ + { "name": "hello" } + ], "resources": { "max_memory_mb": 16, "cpu_budget_ms": 50 diff --git a/Server/plugin/host_commands.go b/Server/plugin/host_commands.go index fd9b972b..c7efc7cc 100644 --- a/Server/plugin/host_commands.go +++ b/Server/plugin/host_commands.go @@ -31,6 +31,14 @@ func (r *Registry) RegisterCommand(cmd string, inst *Instance) error { if !inst.Manifest.HasCapability(CapCommands) { return ErrCapabilityNotGranted } + // Per-command ACL. The `commands` capability alone used to bind whatever + // the guest module returned from list_commands, so an admin enabling a + // plugin could not know which commands it would claim. The manifest is now + // the authority: only declared names bind, and this is the single choke + // point both auto-registration and direct registration route through. + if !inst.Manifest.DeclaresCommand(cmd) { + return fmt.Errorf("%w: %s/%s", ErrCommandNotDeclared, inst.Manifest.Name, cmd) + } r.mu.Lock() defer r.mu.Unlock() // Ownership is compared by plugin identity (manifest name — unique per diff --git a/Server/plugin/host_events.go b/Server/plugin/host_events.go index 2c3c887e..8857af5a 100644 --- a/Server/plugin/host_events.go +++ b/Server/plugin/host_events.go @@ -87,8 +87,22 @@ func (s *EventSink) UnsubscribeAll(inst *Instance) { } } -// Dispatch invokes every subscriber's on_event for topic. The default build -// is a no-op; the wazero-tagged build calls into the WASM module. +// Dispatch invokes every subscriber's on_event for topic. +// +// SECURITY GATE (audit 2026-04-07 finding #4 — "no rate limit on event +// delivery to plugins"). Guest delivery is NOT implemented in either build: +// the loop below touches no module, and nothing in the server calls Dispatch, +// so a plugin cannot slow the hub by handling events slowly. Wiring the +// guest call is what makes the finding real, so whoever does it must land, in +// the same change: +// +// - a per-plugin delivery rate limit (drop, never block the caller), and +// - the same per-call CPU-budget deadline invokeCommand applies +// (sandbox_wazero.go), and +// - delivery off the hub's broadcast goroutine so a slow guest cannot +// backpressure fan-out to WS clients. +// +// Until then this stays inert on purpose. func (s *EventSink) Dispatch(ctx context.Context, topic string, payload []byte) { s.mu.Lock() subs := append([]*Instance(nil), s.subs[topic]...) diff --git a/Server/plugin/host_storage.go b/Server/plugin/host_storage.go index ae31831f..6666beb9 100644 --- a/Server/plugin/host_storage.go +++ b/Server/plugin/host_storage.go @@ -11,7 +11,14 @@ import ( "fmt" ) +// Key isolation is structural rather than checked: every call below passes +// inst.ID as the namespace and there is no parameter by which a caller (let +// alone a guest module) can name a different plugin's namespace. The +// plugin_kv table's PRIMARY KEY (plugin_id, key) — migrations/015_plugins.sql +// — makes the same split the storage layout, so two plugins using the same +// key never collide. Audit 2026-04-07 finding #2. const ( + maxPluginKeyBytes = 256 // 256 B per key maxPluginValueBytes = 64 * 1024 // 64 KB per value maxPluginScanLimit = 1000 // hard cap on PluginKVScan results ) @@ -21,6 +28,12 @@ func (r *Registry) StoragePut(ctx context.Context, inst *Instance, key string, v if !inst.Manifest.HasCapability(CapStorage) { return ErrCapabilityNotGranted } + if key == "" { + return fmt.Errorf("plugin storage: key must not be empty") + } + if len(key) > maxPluginKeyBytes { + return fmt.Errorf("plugin storage: key exceeds %d bytes", maxPluginKeyBytes) + } if len(value) > maxPluginValueBytes { return fmt.Errorf("plugin storage: value exceeds %d bytes", maxPluginValueBytes) } diff --git a/Server/plugin/manifest.go b/Server/plugin/manifest.go index 8eae6faf..922b21a1 100644 --- a/Server/plugin/manifest.go +++ b/Server/plugin/manifest.go @@ -36,18 +36,39 @@ import ( // with a letter or digit. var pluginNameRegexp = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,63}$`) +// pluginCommandRegexp restricts declared slash-command names to the same +// lowercase charset the dispatcher normalizes to (RegisterCommand lowercases +// and strips a leading "/"), so a declaration always compares equal to the +// name a client can actually invoke. +var pluginCommandRegexp = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,31}$`) + +// maxManifestCommands caps how many commands one plugin may claim. +const maxManifestCommands = 64 + // Manifest is the parsed plugin metadata declared in plugin.json (or // plugin.toml in the wazero-tagged build). The on-disk schema is intentionally // flat so the default JSON parser handles it without a TOML dependency. type Manifest struct { - Name string `json:"name"` - Version string `json:"version"` - Author string `json:"author"` - Description string `json:"description"` - Entrypoint string `json:"entrypoint"` // relative .wasm path - Permissions []string `json:"permissions"` - Resources Resources `json:"resources"` - UI UISpec `json:"ui"` + Name string `json:"name"` + Version string `json:"version"` + Author string `json:"author"` + Description string `json:"description"` + Entrypoint string `json:"entrypoint"` // relative .wasm path + Permissions []string `json:"permissions"` + Commands []CommandSpec `json:"commands"` + Resources Resources `json:"resources"` + UI UISpec `json:"ui"` +} + +// CommandSpec is one slash command the plugin declares. The declaration is +// the per-command ACL: activation only binds names listed here, so a guest +// module cannot claim commands its manifest never advertised. +// +// Only Name is enforced today. docs/plans/slash-commands.md extends this +// object with description/options/permission fields later; unknown JSON keys +// are ignored, so manifests written against that richer schema still parse. +type CommandSpec struct { + Name string `json:"name"` } // Resources caps the plugin's runtime budget. Zero means "use the runtime @@ -130,6 +151,9 @@ func (m *Manifest) Validate() error { return fmt.Errorf("plugin manifest: unknown permission %q", p) } } + if err := m.validateCommands(); err != nil { + return err + } if m.Resources.MaxMemoryMB < 0 || m.Resources.CPUBudgetMs < 0 { return fmt.Errorf("plugin manifest: resources must be non-negative") } @@ -180,6 +204,43 @@ func validateRelativePath(p string) error { return nil } +// validateCommands enforces the per-command ACL schema: declaring commands +// requires the `commands` capability, names must be canonical (the form the +// dispatcher normalizes to), and the list is bounded and duplicate-free. +func (m *Manifest) validateCommands() error { + if len(m.Commands) == 0 { + return nil + } + if !m.HasCapability(CapCommands) { + return fmt.Errorf("plugin manifest: commands declared without the %q permission", CapCommands) + } + if len(m.Commands) > maxManifestCommands { + return fmt.Errorf("plugin manifest: too many commands (%d, max %d)", len(m.Commands), maxManifestCommands) + } + seen := make(map[string]bool, len(m.Commands)) + for i, c := range m.Commands { + if !pluginCommandRegexp.MatchString(c.Name) { + return fmt.Errorf("plugin manifest: commands[%d].name %q must match %s", i, c.Name, pluginCommandRegexp.String()) + } + if seen[c.Name] { + return fmt.Errorf("plugin manifest: duplicate command %q", c.Name) + } + seen[c.Name] = true + } + return nil +} + +// DeclaresCommand reports whether the manifest listed cmd in its `commands` +// block. cmd is expected in normalized form (lowercase, no leading "/"). +func (m *Manifest) DeclaresCommand(cmd string) bool { + for _, c := range m.Commands { + if c.Name == cmd { + return true + } + } + return false +} + // HasCapability reports whether the manifest declared cap. func (m *Manifest) HasCapability(cap Capability) bool { for _, p := range m.Permissions { diff --git a/Server/plugin/plugin_test.go b/Server/plugin/plugin_test.go index 4bd6bb47..d93420bb 100644 --- a/Server/plugin/plugin_test.go +++ b/Server/plugin/plugin_test.go @@ -158,7 +158,7 @@ func TestReinstallRebindsCommands(t *testing.T) { t.Cleanup(func() { _ = reg.Close(context.Background()) }) ctx := context.Background() - manifest, err := ParseManifest([]byte(`{"name":"upgrader","version":"0.1.0","entrypoint":"p.wasm","permissions":["commands"]}`)) + manifest, err := ParseManifest([]byte(`{"name":"upgrader","version":"0.1.0","entrypoint":"p.wasm","permissions":["commands"],"commands":[{"name":"greet"}]}`)) if err != nil { t.Fatalf("ParseManifest: %v", err) } @@ -173,7 +173,7 @@ func TestReinstallRebindsCommands(t *testing.T) { } // In-place upgrade: same plugin name, fresh instance. - manifest2, err := ParseManifest([]byte(`{"name":"upgrader","version":"0.2.0","entrypoint":"p.wasm","permissions":["commands"]}`)) + manifest2, err := ParseManifest([]byte(`{"name":"upgrader","version":"0.2.0","entrypoint":"p.wasm","permissions":["commands"],"commands":[{"name":"greet"}]}`)) if err != nil { t.Fatalf("ParseManifest v2: %v", err) } @@ -197,7 +197,7 @@ func TestReinstallRebindsCommands(t *testing.T) { } // A different plugin still cannot hijack an owned command. - other, err := ParseManifest([]byte(`{"name":"other","version":"0.1.0","entrypoint":"o.wasm","permissions":["commands"]}`)) + other, err := ParseManifest([]byte(`{"name":"other","version":"0.1.0","entrypoint":"o.wasm","permissions":["commands"],"commands":[{"name":"greet"}]}`)) if err != nil { t.Fatalf("ParseManifest other: %v", err) } diff --git a/Server/plugin/sandbox_wazero.go b/Server/plugin/sandbox_wazero.go index 508ac585..bf11ee5d 100644 --- a/Server/plugin/sandbox_wazero.go +++ b/Server/plugin/sandbox_wazero.go @@ -131,7 +131,10 @@ func (r *Registry) activateWithRuntime(ctx context.Context, platform any, inst * // the cross-plugin command-hijack hole. RegisterCommand acquires r.mu // itself, so it is called outside the lock below to avoid re-entrant // locking. The plugin must also have declared the `commands` capability in - // its manifest, otherwise no binding happens. + // its manifest, otherwise no binding happens — and RegisterCommand refuses + // any name the manifest's `commands` block did not declare, so a guest + // module cannot widen its own command surface by returning extra names + // from list_commands. r.mu.Lock() if inst.module != nil { // Lost a concurrent activation race (e.g. two dispatches both saw a diff --git a/Server/plugin/sandbox_wazero_test.go b/Server/plugin/sandbox_wazero_test.go index af5077b2..2f96e126 100644 --- a/Server/plugin/sandbox_wazero_test.go +++ b/Server/plugin/sandbox_wazero_test.go @@ -81,7 +81,7 @@ func TestWazeroRegistryCreatesRuntime(t *testing.T) { func TestWazeroActivateCompilesModule(t *testing.T) { dir := t.TempDir() - manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"]}` + manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"hello"}]}` writeTestPlugin(t, dir, "hello", manifest, addWASM) reg, mem := newWazeroTestRegistry(t, dir) @@ -116,7 +116,7 @@ func TestWazeroActivateCompilesModule(t *testing.T) { func TestWazeroDispatchCommandMissingExport(t *testing.T) { dir := t.TempDir() - manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"]}` + manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"hello"}]}` writeTestPlugin(t, dir, "hello", manifest, addWASM) reg, mem := newWazeroTestRegistry(t, dir) @@ -147,7 +147,7 @@ func TestWazeroDispatchCommandMissingExport(t *testing.T) { func TestWazeroCloseTearsDownRuntime(t *testing.T) { dir := t.TempDir() - manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"]}` + manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"hello"}]}` writeTestPlugin(t, dir, "hello", manifest, addWASM) reg, mem := newWazeroTestRegistry(t, dir) @@ -174,7 +174,7 @@ func TestWazeroCloseTearsDownRuntime(t *testing.T) { func TestWazeroDisablePluginFreesModule(t *testing.T) { dir := t.TempDir() - manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"]}` + manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"hello"}]}` writeTestPlugin(t, dir, "hello", manifest, addWASM) reg, mem := newWazeroTestRegistry(t, dir) @@ -270,7 +270,7 @@ var spinWASM = []byte{ // admin disable/enable cycle or server restart. func TestWazeroCPUBudgetOverrunDoesNotBrickPlugin(t *testing.T) { dir := t.TempDir() - manifest := `{"name":"spinner","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"]}` + manifest := `{"name":"spinner","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"spin"}]}` writeTestPlugin(t, dir, "spinner", manifest, spinWASM) reg, mem := newWazeroTestRegistry(t, dir)