Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 10 additions & 4 deletions plugins/openclaw/src/proxy-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ interface LaunchSpec {
const HEADROOM_MODULE_DISCOVERY_SNIPPET =
"import importlib.util, sys; sys.exit(0 if importlib.util.find_spec('headroom') else 1)";

// `python -m` / `python -c` put the current directory first on sys.path, so a
// `headroom/` folder in whatever directory OpenClaw runs from would be used
// instead of the installed package. -P (Python 3.11+, which headroom-ai
// requires) leaves it off; older interpreters reject -P and are skipped.
const PYTHON_SAFE_PATH_FLAG = "-P";

export class ProxyManager {
private config: ProxyManagerConfig;
private logger: ProxyManagerLogger;
Expand Down Expand Up @@ -230,9 +236,9 @@ export class ProxyManager {
specs.push({
label: `Configured Python: ${configuredPython} -m headroom.cli`,
command: configuredPython,
args: ["-m", "headroom.cli", ...commonArgs],
args: [PYTHON_SAFE_PATH_FLAG, "-m", "headroom.cli", ...commonArgs],
checkCommand: configuredPython,
checkArgs: ["-c", HEADROOM_MODULE_DISCOVERY_SNIPPET],
checkArgs: [PYTHON_SAFE_PATH_FLAG, "-c", HEADROOM_MODULE_DISCOVERY_SNIPPET],
});
}

Expand Down Expand Up @@ -325,9 +331,9 @@ export class ProxyManager {
specs.push({
label: `Python: ${pyCmd} -m headroom.cli`,
command: pyCmd,
args: ["-m", "headroom.cli", ...commonArgs],
args: [PYTHON_SAFE_PATH_FLAG, "-m", "headroom.cli", ...commonArgs],
checkCommand: pyCmd,
checkArgs: ["-c", HEADROOM_MODULE_DISCOVERY_SNIPPET],
checkArgs: [PYTHON_SAFE_PATH_FLAG, "-c", HEADROOM_MODULE_DISCOVERY_SNIPPET],
});
}

Expand Down
31 changes: 30 additions & 1 deletion plugins/openclaw/test/proxy-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
isLocalProxyUrl,
probeHeadroomProxy,
} from "../src/proxy-manager.js";
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const retrieveStatsBody = JSON.stringify({ store: { entry_count: 0 }, recent_retrievals: [] });
const proxyStatsBody = JSON.stringify({ proxy_inbound: { total: 1 } });
Expand Down Expand Up @@ -347,7 +351,7 @@ describe("ProxyManager launch internals", () => {

expect(specs[0]?.label).toContain("Configured Python:");
expect(specs[0]?.command).toBe("C:\\Python311\\python.exe");
expect(specs[0]?.args).toEqual(["-m", "headroom.cli", "proxy", "--host", "127.0.0.1", "--port", "8787"]);
expect(specs[0]?.args).toEqual(["-P", "-m", "headroom.cli", "proxy", "--host", "127.0.0.1", "--port", "8787"]);
});

it("uses lightweight PATH checks instead of booting the headroom CLI", () => {
Expand Down Expand Up @@ -407,11 +411,36 @@ describe("ProxyManager launch internals", () => {

expect(pythonSpec).toBeDefined();
expect(pythonSpec?.checkArgs).toEqual([
"-P",
"-c",
"import importlib.util, sys; sys.exit(0 if importlib.util.find_spec('headroom') else 1)",
]);
});

it("never resolves headroom from the working directory in python fallbacks", () => {
const manager = new ProxyManager({ pythonPath: "/opt/py/bin/python3" });
const specs = (manager as any).buildLaunchSpecs("127.0.0.1", "8787") as Array<Record<string, any>>;
const pythonSpecs = specs.filter((spec) => spec.args?.includes("headroom.cli"));
expect(pythonSpecs.length).toBeGreaterThan(1);

const probe = spawnSync("python3", ["-c", "import sys; sys.exit(sys.version_info < (3, 11))"]);
const project = mkdtempSync(join(tmpdir(), "openclaw-cwd-"));
try {
mkdirSync(join(project, "headroom"));
writeFileSync(join(project, "headroom", "__init__.py"), "");
for (const spec of pythonSpecs) {
if (probe.status === 0) {
// -S hides any installed headroom, so only the project folder could match.
const check = spawnSync("python3", ["-S", ...spec.checkArgs], { cwd: project });
expect(check.status).toBe(1);
Comment thread
chopratejas marked this conversation as resolved.
Resolved
}
expect(spec.args[0]).toBe("-P");
}
} finally {
rmSync(project, { recursive: true, force: true });
}
});

it("uses first available launcher from provided specs", async () => {
const manager = new ProxyManager({});
(manager as any).buildLaunchSpecs = () => [
Expand Down
Loading