diff --git a/CHANGELOG.md b/CHANGELOG.md
index c58b9d4160e..eef46588432 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
- Gateway/config: stop Gateway startup and hot reload from auto-restoring invalid config; invalid config now fails closed and `openclaw doctor --fix` owns last-known-good repair.
- Gateway/performance: lazy-load early runtime discovery and shutdown-hook helpers, defer maintenance timers until after readiness, and trim duplicate plugin auto-enable work during Gateway startup.
- QA/Mantis: add a `pnpm openclaw qa mantis discord-smoke` runner and manual GitHub workflow that verify the Mantis Discord bot can see the configured guild/channel, post a smoke message, add a reaction, and upload artifacts.
+- QA/Mantis: pass the runtime env through desktop-browser Crabbox and artifact-copy child commands, so embedded Mantis callers can provide Crabbox credentials without mutating the parent process. Thanks @vincentkoc.
- QA/Slack: add a Slack live transport QA runner with canary and mention-gating coverage for the private bot-to-bot harness. Thanks @vincentkoc.
- Gateway/performance: lazy-load the heavy cron runtime after the rest of Gateway startup, defer restart-sentinel refresh after readiness, and let the Gateway startup benchmark write per-run V8 CPU profiles with `--cpu-prof-dir`.
- Gateway/performance: keep raw channel-config schema parsing from discovering bundled plugin runtime metadata, and add `pnpm gateway:watch --benchmark-no-force` for profiling startup without the default port cleanup.
diff --git a/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.test.ts b/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.test.ts
index 4c4bf9fd253..d2c6623555e 100644
--- a/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.test.ts
+++ b/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.test.ts
@@ -18,43 +18,51 @@ describe("mantis desktop browser smoke runtime", () => {
it("leases a desktop box, runs a visible browser, copies artifacts, and stops on pass", async () => {
await fs.mkdir(path.join(repoRoot, "qa-artifacts"), { recursive: true });
await fs.writeFile(path.join(repoRoot, "qa-artifacts", "timeline.html"), "
Mantis
");
- const commands: { args: readonly string[]; command: string }[] = [];
- const runner = vi.fn(async (command: string, args: readonly string[]) => {
- commands.push({ command, args });
- if (command === "/tmp/crabbox" && args[0] === "warmup") {
- return { stdout: "ready lease cbx_abc123\n", stderr: "" };
- }
- if (command === "/tmp/crabbox" && args[0] === "inspect") {
- return {
- stdout: `${JSON.stringify({
- host: "203.0.113.10",
- id: "cbx_abc123",
- provider: "hetzner",
- slug: "brisk-mantis",
- sshKey: "/tmp/key",
- sshPort: "2222",
- sshUser: "crabbox",
- state: "active",
- })}\n`,
- stderr: "",
- };
- }
- if (command === "rsync") {
- const outputDir = args.at(-1);
- expect(outputDir).toBeTypeOf("string");
- await fs.mkdir(outputDir as string, { recursive: true });
- await fs.writeFile(path.join(outputDir as string, "desktop-browser-smoke.png"), "png");
- await fs.writeFile(path.join(outputDir as string, "remote-metadata.json"), "{}\n");
- await fs.writeFile(path.join(outputDir as string, "chrome.log"), "chrome\n");
+ const commands: { args: readonly string[]; command: string; env?: NodeJS.ProcessEnv }[] = [];
+ const runtimeEnv = {
+ PATH: process.env.PATH,
+ CRABBOX_COORDINATOR_TOKEN: "runtime-token",
+ OPENCLAW_MANTIS_CRABBOX_PROVIDER: "hetzner",
+ };
+ const runner = vi.fn(
+ async (command: string, args: readonly string[], options: { env?: NodeJS.ProcessEnv }) => {
+ commands.push({ command, args, env: options.env });
+ if (command === "/tmp/crabbox" && args[0] === "warmup") {
+ return { stdout: "ready lease cbx_abc123\n", stderr: "" };
+ }
+ if (command === "/tmp/crabbox" && args[0] === "inspect") {
+ return {
+ stdout: `${JSON.stringify({
+ host: "203.0.113.10",
+ id: "cbx_abc123",
+ provider: "hetzner",
+ slug: "brisk-mantis",
+ sshKey: "/tmp/key",
+ sshPort: "2222",
+ sshUser: "crabbox",
+ state: "active",
+ })}\n`,
+ stderr: "",
+ };
+ }
+ if (command === "rsync") {
+ const outputDir = args.at(-1);
+ expect(outputDir).toBeTypeOf("string");
+ await fs.mkdir(outputDir as string, { recursive: true });
+ await fs.writeFile(path.join(outputDir as string, "desktop-browser-smoke.png"), "png");
+ await fs.writeFile(path.join(outputDir as string, "remote-metadata.json"), "{}\n");
+ await fs.writeFile(path.join(outputDir as string, "chrome.log"), "chrome\n");
+ return { stdout: "", stderr: "" };
+ }
return { stdout: "", stderr: "" };
- }
- return { stdout: "", stderr: "" };
- });
+ },
+ );
const result = await runMantisDesktopBrowserSmoke({
browserUrl: "https://openclaw.ai/docs",
commandRunner: runner,
crabboxBin: "/tmp/crabbox",
+ env: runtimeEnv,
htmlFile: "qa-artifacts/timeline.html",
now: () => new Date("2026-05-04T12:00:00.000Z"),
outputDir: ".artifacts/qa-e2e/mantis/desktop-browser-test",
@@ -69,6 +77,7 @@ describe("mantis desktop browser smoke runtime", () => {
["rsync", "-az"],
["/tmp/crabbox", "stop"],
]);
+ expect(commands.every((entry) => entry.env === runtimeEnv)).toBe(true);
const rsyncArgs = commands.find((entry) => entry.command === "rsync")?.args ?? [];
expect(rsyncArgs).not.toContain("--delete");
expect(rsyncArgs).toEqual(
diff --git a/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts b/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts
index 6c4da15ed38..81c531a3c4d 100644
--- a/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts
+++ b/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts
@@ -303,12 +303,13 @@ async function runCommand(params: {
args: readonly string[];
command: string;
cwd: string;
+ env: NodeJS.ProcessEnv;
runner: CommandRunner;
stdio?: "inherit" | "pipe";
}) {
return params.runner(params.command, params.args, {
cwd: params.cwd,
- env: process.env,
+ env: params.env,
stdio: params.stdio ?? "pipe",
});
}
@@ -316,6 +317,7 @@ async function runCommand(params: {
async function warmupCrabbox(params: {
crabboxBin: string;
cwd: string;
+ env: NodeJS.ProcessEnv;
idleTimeout: string;
machineClass: string;
provider: string;
@@ -338,6 +340,7 @@ async function warmupCrabbox(params: {
params.ttl,
],
cwd: params.cwd,
+ env: params.env,
runner: params.runner,
stdio: "inherit",
});
@@ -351,6 +354,7 @@ async function warmupCrabbox(params: {
async function inspectCrabbox(params: {
crabboxBin: string;
cwd: string;
+ env: NodeJS.ProcessEnv;
leaseId: string;
provider: string;
runner: CommandRunner;
@@ -359,6 +363,7 @@ async function inspectCrabbox(params: {
command: params.crabboxBin,
args: ["inspect", "--provider", params.provider, "--id", params.leaseId, "--json"],
cwd: params.cwd,
+ env: params.env,
runner: params.runner,
});
return JSON.parse(result.stdout) as CrabboxInspect;
@@ -366,6 +371,7 @@ async function inspectCrabbox(params: {
async function copyRemoteArtifacts(params: {
cwd: string;
+ env: NodeJS.ProcessEnv;
inspect: CrabboxInspect;
outputDir: string;
remoteOutputDir: string;
@@ -401,6 +407,7 @@ async function copyRemoteArtifacts(params: {
`${params.outputDir}/`,
],
cwd: params.cwd,
+ env: params.env,
runner: params.runner,
});
}
@@ -408,6 +415,7 @@ async function copyRemoteArtifacts(params: {
async function stopCrabbox(params: {
crabboxBin: string;
cwd: string;
+ env: NodeJS.ProcessEnv;
leaseId: string;
provider: string;
runner: CommandRunner;
@@ -416,6 +424,7 @@ async function stopCrabbox(params: {
command: params.crabboxBin,
args: ["stop", "--provider", params.provider, params.leaseId],
cwd: params.cwd,
+ env: params.env,
runner: params.runner,
stdio: "inherit",
});
@@ -471,6 +480,7 @@ export async function runMantisDesktopBrowserSmoke(
(await warmupCrabbox({
crabboxBin,
cwd: repoRoot,
+ env,
idleTimeout,
machineClass,
provider,
@@ -480,6 +490,7 @@ export async function runMantisDesktopBrowserSmoke(
const inspected = await inspectCrabbox({
crabboxBin,
cwd: repoRoot,
+ env,
leaseId,
provider,
runner,
@@ -500,11 +511,13 @@ export async function runMantisDesktopBrowserSmoke(
renderRemoteScript({ browserUrl, htmlBase64, remoteOutputDir }),
],
cwd: repoRoot,
+ env,
runner,
stdio: "inherit",
});
await copyRemoteArtifacts({
cwd: repoRoot,
+ env,
inspect: inspected,
outputDir,
remoteOutputDir,
@@ -582,7 +595,7 @@ export async function runMantisDesktopBrowserSmoke(
await fs.writeFile(reportPath, renderReport(summary), "utf8");
}
if (summary?.status === "pass" && createdLease && leaseId && !keepLease) {
- await stopCrabbox({ crabboxBin, cwd: repoRoot, leaseId, provider, runner });
+ await stopCrabbox({ crabboxBin, cwd: repoRoot, env, leaseId, provider, runner });
}
}
}