diff --git a/CHANGELOG.md b/CHANGELOG.md index b2448fff6..b14921f56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - chore: update json-as and remove hack [#857](https://github.com/hypermodeinc/modus/pull/857) - chore: rename agent lifecycle methods and APIs [#858](https://github.com/hypermodeinc/modus/pull/858) +- feat: enforce WASI reactor mode [#859](https://github.com/hypermodeinc/modus/pull/859) ## 2025-05-22 - Go SDK 0.18.0-alpha.3 diff --git a/cli/src/custom/globals.ts b/cli/src/custom/globals.ts index 30d795ccc..8dd569177 100644 --- a/cli/src/custom/globals.ts +++ b/cli/src/custom/globals.ts @@ -15,7 +15,7 @@ export const ModusHomeDir = process.env.MODUS_HOME || path.join(os.homedir(), ". export const MinNodeVersion = "22.0.0"; export const MinGoVersion = "1.23.1"; -export const MinTinyGoVersion = "0.33.0"; +export const MinTinyGoVersion = "0.35.0"; export const GitHubOwner = "hypermodeinc"; export const GitHubRepo = "modus"; diff --git a/runtime/plugins/plugins.go b/runtime/plugins/plugins.go index 02aa82b87..a57f8075b 100644 --- a/runtime/plugins/plugins.go +++ b/runtime/plugins/plugins.go @@ -30,6 +30,7 @@ type Plugin struct { FileName string Language langsupport.Language ExecutionPlans map[string]langsupport.ExecutionPlan + StartFunction string } func NewPlugin(ctx context.Context, cm wazero.CompiledModule, filename string, md *metadata.Metadata) (*Plugin, error) { @@ -83,6 +84,20 @@ func NewPlugin(ctx context.Context, cm wazero.CompiledModule, filename string, m plans[importName] = plan } + var startFunction string + if _, found := exports["_initialize"]; found { + // all modules should be reactors, but prior to v0.18, some modules were not. + startFunction = "_initialize" + } else if _, found := exports["_start"]; found { + // this will happen if the module was compiled using TinyGo < 0.35, or Modus AssemblyScript SDK < v0.18.0-alpha.3 + startFunction = "_start" + logger.Warn(ctx).Bool("user_visible", true). + Msgf("%s is not correctly configured as a WASI reactor module. Please rebuild the Modus app using the latest version of the Modus SDK.", filename) + } else { + // this path would only occur if the module was not compiled using a Modus SDK + return nil, fmt.Errorf("no WASI startup function found in %s", filename) + } + plugin := &Plugin{ Id: utils.GenerateUUIDv7(), Module: cm, @@ -90,6 +105,7 @@ func NewPlugin(ctx context.Context, cm wazero.CompiledModule, filename string, m FileName: filename, Language: language, ExecutionPlans: plans, + StartFunction: startFunction, } return plugin, nil diff --git a/runtime/wasmhost/wasmhost.go b/runtime/wasmhost/wasmhost.go index cfefb149f..2afcf438b 100644 --- a/runtime/wasmhost/wasmhost.go +++ b/runtime/wasmhost/wasmhost.go @@ -106,7 +106,7 @@ func (host *wasmHost) GetModuleInstance(ctx context.Context, plugin *plugins.Plu span, ctx := utils.NewSentrySpanForCurrentFunc(ctx) defer span.Finish() - cfg := getModuleConfig(ctx, buffers) + cfg := getModuleConfig(ctx, buffers, plugin) mod, err := host.runtime.InstantiateModule(ctx, plugin.Module, cfg) if err != nil { return nil, fmt.Errorf("failed to instantiate the plugin module: %w", err) @@ -127,7 +127,7 @@ func (host *wasmHost) CompileModule(ctx context.Context, bytes []byte) (wazero.C return cm, nil } -func getModuleConfig(ctx context.Context, buffers utils.OutputBuffers) wazero.ModuleConfig { +func getModuleConfig(ctx context.Context, buffers utils.OutputBuffers, plugin *plugins.Plugin) wazero.ModuleConfig { // Get the logger and writers for the plugin's stdout and stderr. log := logger.Get(ctx).With().Bool("user_visible", true).Logger() @@ -155,7 +155,7 @@ func getModuleConfig(ctx context.Context, buffers utils.OutputBuffers) wazero.Mo // And https://gophers.slack.com/archives/C040AKTNTE0/p1719587772724619?thread_ts=1719522663.531579&cid=C040AKTNTE0 cfg := wazero.NewModuleConfig(). WithName(""). - WithStartFunctions("_initialize", "_start"). + WithStartFunctions(plugin.StartFunction). WithSysWalltime().WithSysNanotime(). WithRandSource(rand.Reader). WithStdout(wOut).WithStderr(wErr). diff --git a/sdk/assemblyscript/src/plugin.asconfig.json b/sdk/assemblyscript/src/plugin.asconfig.json index c75a2b771..d8ac8a170 100644 --- a/sdk/assemblyscript/src/plugin.asconfig.json +++ b/sdk/assemblyscript/src/plugin.asconfig.json @@ -15,7 +15,7 @@ "process=wasi_process", "Date=wasi_Date" ], - "exportStart": "_start", + "exportStart": "_initialize", "exportRuntime": true }, "targets": { diff --git a/sdk/go/tools/modus-go-build/compiler/compiler.go b/sdk/go/tools/modus-go-build/compiler/compiler.go index ec0bc8d99..0643853c0 100644 --- a/sdk/go/tools/modus-go-build/compiler/compiler.go +++ b/sdk/go/tools/modus-go-build/compiler/compiler.go @@ -21,26 +21,16 @@ import ( "github.com/hashicorp/go-version" ) -const minTinyGoVersion = "0.33.0" +const minTinyGoVersion = "0.35.0" func Compile(config *config.Config) error { - tinygoVersion, err := getCompilerVersion(config) - if err != nil { - return err - } - args := []string{"build"} args = append(args, "-target", "wasip1") args = append(args, "-o", filepath.Join(config.OutputDir, config.WasmFileName)) - // WASI "reactor mode" (-buildmode=c-shared) is required for TinyGo 0.35.0 and later. - // Otherwise, the _start function runs and immediately exits before any function can execute. - // This also switches the startup function to _initialize instead of _start, so the Modus runtime - // needs to match. - if tinygoVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.35.0"))) { - args = append(args, "-buildmode", "c-shared") - } + // build a WASI reactor module - not a command module + args = append(args, "-buildmode", "c-shared") // disable the asyncify scheduler until we better understand how to use it args = append(args, "-scheduler", "none")