Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions agent_subrun/launch_wbtest.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
///|
/// `launch_argv` decides how an assembled child argv reaches the OS. The
/// wasm rewrite is what keeps sub-runs alive under `moonx`, and it has to
/// leave every non-wasm child — real binaries and the scripted test children
/// alike — exactly as it found it.

///|
test "a wasm engine is launched through its runtime" {
let (command, args) = launch_argv("/cache/openseek.wasm", [
"subrun", "explore", "--model", "deepseek-v4-flash",
])
assert_eq(command, "moonrun")
// The module comes first and `--` fences it off, or moonrun would read the
// engine's own flags as its own.
assert_eq(args, [
"/cache/openseek.wasm", "--", "subrun", "explore", "--model", "deepseek-v4-flash",
])
}

///|
test "a childless argv still gets the fence" {
let (command, args) = launch_argv("/cache/openseek.wasm", [])
assert_eq(command, "moonrun")
assert_eq(args, ["/cache/openseek.wasm", "--"])
}

///|
test "a native engine is spawned directly" {
let argv = ["subrun", "review"]
let (command, args) = launch_argv("/usr/local/bin/openseek", argv)
assert_eq(command, "/usr/local/bin/openseek")
assert_eq(args, argv)
}

///|
/// The scripted children the runner tests use, and a PATH-resolved bare name,
/// must be untouched: the suffix is the only trigger.
test "scripted and PATH-resolved children are untouched" {
let (sh, sh_args) = launch_argv("sh", ["-c", "echo hi"])
assert_eq(sh, "sh")
assert_eq(sh_args, ["-c", "echo hi"])
let (bare, bare_args) = launch_argv("openseek", ["subrun", "explore"])
assert_eq(bare, "openseek")
assert_eq(bare_args, ["subrun", "explore"])
}

///|
/// `.wasm` has to be the SUFFIX, not merely present: a directory named for
/// the backend, or a binary whose name embeds it, is still a real executable.
test "only a trailing .wasm triggers the rewrite" {
let (in_dir, _) = launch_argv("/build/wasm/openseek", [])
assert_eq(in_dir, "/build/wasm/openseek")
let (named, _) = launch_argv("/build/openseek.wasm.bak", [])
assert_eq(named, "/build/openseek.wasm.bak")
}
38 changes: 38 additions & 0 deletions agent_subrun/runner.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,41 @@ pub fn[T] SubrunResult::subrun_id(self : SubrunResult[T]) -> String {
self.subrun_id
}

///|
/// How `command` actually gets launched.
///
/// A native build spawns its own binary directly. A wasm build cannot: there
/// the parent's argv[0] is a `.wasm` module, which no OS will `exec`, so every
/// sub-run under `moonx` died at spawn with `@process.spawn(): Permission
/// denied` — taking the `explore`, `review`, and `subtask` tools and the
/// `--review-gate` audit with it, while the parent turn itself worked fine.
/// The module needs its runtime in front of it, `moonrun <module> --
/// <args...>`, which is the same shape `moonx` used to start the parent.
///
/// The runtime is NAMED, not discovered, and that is the part worth
/// justifying. Runtimes that host a program usually hand it a way back to
/// themselves — Node has `process.execPath`, Python `sys.executable`, Ruby
/// `RbConfig.ruby` — and self-respawn is supposed to go through that. wasm
/// has no equivalent: a module cannot ask who is hosting it. There is also
/// nothing to choose between, since `moonrun` is the only host supplying the
/// FFI this binary needs (process spawn, sockets, filesystem) — a bare WASI
/// runtime could not run the parent either, so it can never be the answer.
///
/// The suffix test reads the CHILD path, not the parent's own backend,
/// deliberately: it stays right for a native parent pointed at a wasm engine,
/// and leaves the scripted test children (`sh`, absolute paths, bare names)
/// untouched.
fn launch_argv(
command : String,
args : Array[String],
) -> (String, Array[String]) {
if command.has_suffix(".wasm") {
("moonrun", [command, "--", ..args])
} else {
(command, args)
}
}

///|
/// Run one sub-run in a DEDICATED CHILD PROCESS and report what happened and
/// what it cost.
Expand Down Expand Up @@ -125,6 +160,9 @@ pub async fn[T] run_subrun(
[..args, "--session", "\{spec.parent}-\{id}", "--session-root", spec.root]
None => args
}
// Assembled argv in hand, decide how it actually gets launched: a wasm
// engine needs its runtime in front of it, a native one does not.
let (command, args) = launch_argv(command, args)
emit_event(
SubrunStarted(id~, kind~, label=@agent_tool.brief_line(label, limit=72)),
)
Expand Down
4 changes: 4 additions & 0 deletions cmd/openseek/subrun.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,10 @@ async fn dispatch_kind(
/// name stays PATH-resolved, matching however this process was launched.
/// Never hardcode a name: parent and child must be the SAME binary, or the
/// typed report channel's same-codec guarantee is gone.
///
/// On a wasm build this returns the `.wasm` module's own path, which is not
/// executable on its own; `agent_subrun`'s `launch_argv` puts `moonrun` in
/// front of it at the spawn boundary.
async fn self_executable() -> String {
let argv0 = match @env.args() {
[first, ..] => first
Expand Down
Loading