mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 20:52:15 +03:00
* feat(cli): refuse ephemeral container auto-config writes Detect containerized OmniRoute and block CLI/API config writes into throwaway homes unless a bind mount or explicit opt-in is present, and honor compose host-profile CLI_CONFIG_HOME mounts outside the container home. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(changelog): name fragment for #10057 Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: yansigit <yansigit@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
150 lines
6.0 KiB
TypeScript
150 lines
6.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
// The compose `host` profile mounts the operator's real config dirs at
|
|
// /host-home, which sits OUTSIDE the container user's home (/home/node). Before
|
|
// this fix getCliConfigHome() silently dropped that override and every write
|
|
// landed back in the ephemeral container home. See docker-compose.yml.
|
|
|
|
const modulePath = path.join(process.cwd(), "src/shared/services/cliRuntime.ts");
|
|
const originalEnv = { ...process.env };
|
|
|
|
async function importFresh(label: string) {
|
|
return import(`${pathToFileURL(modulePath).href}?case=${label}-${Date.now()}-${Math.random()}`);
|
|
}
|
|
|
|
function restoreEnv() {
|
|
for (const key of Object.keys(process.env)) {
|
|
if (!(key in originalEnv)) delete process.env[key];
|
|
}
|
|
Object.assign(process.env, originalEnv);
|
|
}
|
|
|
|
test.afterEach(restoreEnv);
|
|
|
|
// Deps are injected because CI and dev machines are not containers and macOS
|
|
// has no /proc/self/mountinfo at all.
|
|
const HOST_PROFILE_MOUNTINFO = [
|
|
"31 28 254:1 /volumes/omniroute-data/_data /app/data rw,relatime - ext4 /dev/vda1 rw",
|
|
"44 28 254:1 /Users/me/.codex /host-home/.codex rw,relatime - ext4 /dev/vda1 rw",
|
|
"45 28 254:1 /Users/me/.claude /host-home/.claude rw,relatime - ext4 /dev/vda1 rw",
|
|
].join("\n");
|
|
|
|
const containerDeps = {
|
|
existsSync: (p: string) => p === "/.dockerenv",
|
|
readFileSync: (p: string, _enc: string) => {
|
|
if (p === "/proc/self/mountinfo") return HOST_PROFILE_MOUNTINFO;
|
|
if (p === "/proc/1/cgroup") return "12:cpuset:/docker/abc\n";
|
|
throw new Error("ENOENT");
|
|
},
|
|
env: {} as NodeJS.ProcessEnv,
|
|
};
|
|
|
|
const hostDeps = {
|
|
existsSync: (_p: string) => false,
|
|
readFileSync: (_p: string, _enc: string) => "12:cpuset:/\n",
|
|
env: {} as NodeJS.ProcessEnv,
|
|
};
|
|
|
|
test("container + bind-mounted CLI_CONFIG_HOME outside home is honoured", async () => {
|
|
const cliRuntime = await importFresh("container-mounted");
|
|
process.env.CLI_CONFIG_HOME = "/host-home";
|
|
assert.equal(cliRuntime.getCliConfigHome(containerDeps), "/host-home");
|
|
});
|
|
|
|
test("container + unmounted CLI_CONFIG_HOME outside home still falls back", async () => {
|
|
const cliRuntime = await importFresh("container-unmounted");
|
|
process.env.CLI_CONFIG_HOME = "/opt/not-mounted";
|
|
assert.equal(cliRuntime.getCliConfigHome(containerDeps), os.homedir());
|
|
});
|
|
|
|
test("host machine keeps rejecting an outside-home CLI_CONFIG_HOME", async () => {
|
|
const cliRuntime = await importFresh("host-outside");
|
|
process.env.CLI_CONFIG_HOME = "/tmp/outside-home";
|
|
assert.equal(cliRuntime.getCliConfigHome(hostDeps), os.homedir());
|
|
// ...and with the real (non-container) environment too.
|
|
assert.equal(cliRuntime.getCliConfigHome(), os.homedir());
|
|
});
|
|
|
|
test("container exception does not bypass the other CLI_CONFIG_HOME guards", async () => {
|
|
const cliRuntime = await importFresh("container-guards");
|
|
const home = os.homedir();
|
|
|
|
process.env.CLI_CONFIG_HOME = "relative/host-home";
|
|
assert.equal(cliRuntime.getCliConfigHome(containerDeps), home, "relative paths rejected");
|
|
|
|
process.env.CLI_CONFIG_HOME = "/host-home/../etc";
|
|
assert.equal(cliRuntime.getCliConfigHome(containerDeps), home, "traversal rejected");
|
|
|
|
process.env.CLI_CONFIG_HOME = "/host-home;rm -rf /";
|
|
assert.equal(cliRuntime.getCliConfigHome(containerDeps), home, "metacharacters rejected");
|
|
});
|
|
|
|
test("an in-home CLI_CONFIG_HOME is unaffected by container detection", async () => {
|
|
const cliRuntime = await importFresh("in-home");
|
|
const safe = path.join(os.homedir(), "tmp-cli-config-home");
|
|
process.env.CLI_CONFIG_HOME = safe;
|
|
assert.equal(cliRuntime.getCliConfigHome(containerDeps), safe);
|
|
assert.equal(cliRuntime.getCliConfigHome(hostDeps), safe);
|
|
});
|
|
|
|
// ── ensureCliConfigWriteAllowed ──────────────────────────────────────────────
|
|
|
|
test("ensureCliConfigWriteAllowed without a path keeps flag-only behavior", async () => {
|
|
const cliRuntime = await importFresh("gate-flag-only");
|
|
assert.equal(cliRuntime.ensureCliConfigWriteAllowed(), null);
|
|
|
|
process.env.CLI_ALLOW_CONFIG_WRITES = "false";
|
|
assert.match(cliRuntime.ensureCliConfigWriteAllowed(), /CLI_ALLOW_CONFIG_WRITES=false/);
|
|
});
|
|
|
|
test("ensureCliConfigWriteAllowed refuses an ephemeral container target", async () => {
|
|
const cliRuntime = await importFresh("gate-ephemeral");
|
|
const message = cliRuntime.ensureCliConfigWriteAllowed("/home/node/.codex", { containerDeps });
|
|
assert.ok(message, "expected a refusal");
|
|
assert.match(message, /Refusing to write/);
|
|
assert.match(message, /\/home\/node\/\.codex/);
|
|
assert.match(message, /omniroute connect/);
|
|
assert.match(message, /CLI_CONFIG_HOME=\/host-home/);
|
|
});
|
|
|
|
test("ensureCliConfigWriteAllowed allows a bind-mounted container target", async () => {
|
|
const cliRuntime = await importFresh("gate-mounted");
|
|
assert.equal(
|
|
cliRuntime.ensureCliConfigWriteAllowed("/host-home/.codex/config.toml", { containerDeps }),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("ensureCliConfigWriteAllowed allows any target on a host", async () => {
|
|
const cliRuntime = await importFresh("gate-host");
|
|
assert.equal(
|
|
cliRuntime.ensureCliConfigWriteAllowed(path.join(os.homedir(), ".codex"), {
|
|
containerDeps: hostDeps,
|
|
}),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("OMNIROUTE_ALLOW_CONTAINER_CONFIG_WRITE overrides the container refusal", async () => {
|
|
const cliRuntime = await importFresh("gate-override");
|
|
process.env.OMNIROUTE_ALLOW_CONTAINER_CONFIG_WRITE = "true";
|
|
assert.equal(
|
|
cliRuntime.ensureCliConfigWriteAllowed("/home/node/.codex", { containerDeps }),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("the write-disabled flag still wins over the container override", async () => {
|
|
const cliRuntime = await importFresh("gate-precedence");
|
|
process.env.CLI_ALLOW_CONFIG_WRITES = "false";
|
|
process.env.OMNIROUTE_ALLOW_CONTAINER_CONFIG_WRITE = "true";
|
|
assert.match(
|
|
cliRuntime.ensureCliConfigWriteAllowed("/home/node/.codex", { containerDeps }),
|
|
/CLI_ALLOW_CONFIG_WRITES=false/
|
|
);
|
|
});
|