mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(config): surface helpful chown hint on EACCES when reading config
When the gateway is deployed in a Docker/container environment using a
1-click hosting template, the openclaw.json config file can end up owned
by root (mode 600) while the gateway process runs as the non-root 'node'
user. This causes a silent EACCES failure: the gateway starts with an
empty config and Telegram/Discord bots stop responding.
Before this fix the error was logged as a generic 'read failed: ...'
message with no indication of how to recover.
After this fix:
- EACCES errors log a clear, actionable error to stderr (visible in
docker logs) with the exact chown command to run
- The config snapshot issue message also includes the chown hint so
'openclaw gateway status' / Control UI surface the fix path
- process.getuid() is used to include the current UID in the hint;
falls back to '1001' on platforms where it is unavailable
Fixes #24853
(cherry picked from commit 0a3c572c41)
This commit is contained in:
committed by
Peter Steinberger
parent
f3459d71e8
commit
c69fc383b9
60
src/config/io.eacces.test.ts
Normal file
60
src/config/io.eacces.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createConfigIO } from "./io.js";
|
||||
|
||||
function makeEaccesFs(configPath: string) {
|
||||
const eaccesErr = Object.assign(new Error(`EACCES: permission denied, open '${configPath}'`), {
|
||||
code: "EACCES",
|
||||
});
|
||||
return {
|
||||
existsSync: (p: string) => p === configPath,
|
||||
readFileSync: (p: string): string => {
|
||||
if (p === configPath) {
|
||||
throw eaccesErr;
|
||||
}
|
||||
throw new Error(`unexpected readFileSync: ${p}`);
|
||||
},
|
||||
promises: {
|
||||
readFile: () => Promise.reject(eaccesErr),
|
||||
mkdir: () => Promise.resolve(),
|
||||
writeFile: () => Promise.resolve(),
|
||||
appendFile: () => Promise.resolve(),
|
||||
},
|
||||
} as unknown as typeof import("node:fs").default;
|
||||
}
|
||||
|
||||
describe("config io EACCES handling", () => {
|
||||
it("returns a helpful error message when config file is not readable (EACCES)", async () => {
|
||||
const configPath = "/data/.openclaw/openclaw.json";
|
||||
const errors: string[] = [];
|
||||
const io = createConfigIO({
|
||||
configPath,
|
||||
fs: makeEaccesFs(configPath),
|
||||
logger: {
|
||||
error: (msg: unknown) => errors.push(String(msg)),
|
||||
warn: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const snapshot = await io.readConfigFileSnapshot();
|
||||
expect(snapshot.valid).toBe(false);
|
||||
expect(snapshot.issues).toHaveLength(1);
|
||||
expect(snapshot.issues[0].message).toContain("EACCES");
|
||||
expect(snapshot.issues[0].message).toContain("chown");
|
||||
expect(snapshot.issues[0].message).toContain(configPath);
|
||||
// Should also emit to the logger
|
||||
expect(errors.some((e) => e.includes("chown"))).toBe(true);
|
||||
});
|
||||
|
||||
it("includes configPath in the chown hint for the correct remediation command", async () => {
|
||||
const configPath = "/home/myuser/.openclaw/openclaw.json";
|
||||
const io = createConfigIO({
|
||||
configPath,
|
||||
fs: makeEaccesFs(configPath),
|
||||
logger: { error: () => {}, warn: () => {} },
|
||||
});
|
||||
|
||||
const snapshot = await io.readConfigFileSnapshot();
|
||||
expect(snapshot.issues[0].message).toContain(configPath);
|
||||
expect(snapshot.issues[0].message).toContain("container");
|
||||
});
|
||||
});
|
||||
@@ -936,6 +936,25 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
envSnapshotForRestore: readResolution.envSnapshotForRestore,
|
||||
};
|
||||
} catch (err) {
|
||||
const nodeErr = err as NodeJS.ErrnoException;
|
||||
let message: string;
|
||||
if (nodeErr?.code === "EACCES") {
|
||||
// Permission denied — common in Docker/container deployments where the
|
||||
// config file is owned by root but the gateway runs as a non-root user.
|
||||
const uid = process.getuid?.();
|
||||
const uidHint = typeof uid === "number" ? String(uid) : "$(id -u)";
|
||||
message = [
|
||||
`read failed: ${String(err)}`,
|
||||
``,
|
||||
`Config file is not readable by the current process. If running in a container`,
|
||||
`or 1-click deployment, fix ownership with:`,
|
||||
` chown ${uidHint} "${configPath}"`,
|
||||
`Then restart the gateway.`,
|
||||
].join("\n");
|
||||
deps.logger.error(message);
|
||||
} else {
|
||||
message = `read failed: ${String(err)}`;
|
||||
}
|
||||
return {
|
||||
snapshot: {
|
||||
path: configPath,
|
||||
@@ -946,7 +965,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
valid: false,
|
||||
config: {},
|
||||
hash: hashConfigRaw(null),
|
||||
issues: [{ path: "", message: `read failed: ${String(err)}` }],
|
||||
issues: [{ path: "", message }],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user