mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
docs(config): clarify symlinked config support
This commit is contained in:
@@ -359,6 +359,9 @@ If dry-run fails:
|
||||
post-change config before committing it to disk. If the new payload fails schema
|
||||
validation or looks like a destructive clobber, the active config is left alone
|
||||
and the rejected payload is saved beside it as `openclaw.json.rejected.*`.
|
||||
The active config path must be a regular file. Symlinked `openclaw.json`
|
||||
layouts are unsupported for writes; use `OPENCLAW_CONFIG_PATH` to point directly
|
||||
at the real file instead.
|
||||
|
||||
Prefer CLI writes for small edits:
|
||||
|
||||
@@ -383,7 +386,7 @@ last-known-good backup during startup or hot reload. See
|
||||
|
||||
## Subcommands
|
||||
|
||||
- `config file`: Print the active config file path (resolved from `OPENCLAW_CONFIG_PATH` or default location).
|
||||
- `config file`: Print the active config file path (resolved from `OPENCLAW_CONFIG_PATH` or default location). The path should name a regular file, not a symlink.
|
||||
|
||||
Restart the gateway after edits.
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ title: "Configuration"
|
||||
# Configuration
|
||||
|
||||
OpenClaw reads an optional <Tooltip tip="JSON5 supports comments and trailing commas">**JSON5**</Tooltip> config from `~/.openclaw/openclaw.json`.
|
||||
The active config path must be a regular file. Symlinked `openclaw.json`
|
||||
layouts are unsupported for OpenClaw-owned writes; an atomic write may replace
|
||||
the path instead of preserving the symlink. If you keep config outside the
|
||||
default state directory, point `OPENCLAW_CONFIG_PATH` directly at the real file.
|
||||
|
||||
If the file is missing, OpenClaw uses safe defaults. Common reasons to add a config:
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ High-signal `checkId` values you will most likely see in real deployments (not e
|
||||
| `fs.state_dir.perms_readable` | warn | State dir is readable by others | filesystem perms on `~/.openclaw` | yes |
|
||||
| `fs.state_dir.symlink` | warn | State dir target becomes another trust boundary | state dir filesystem layout | no |
|
||||
| `fs.config.perms_writable` | critical | Others can change auth/tool policy/config | filesystem perms on `~/.openclaw/openclaw.json` | yes |
|
||||
| `fs.config.symlink` | warn | Config target becomes another trust boundary | config file filesystem layout | no |
|
||||
| `fs.config.symlink` | warn | Symlinked config files are unsupported for writes and add another trust boundary | replace with a regular config file or point `OPENCLAW_CONFIG_PATH` at the real file | no |
|
||||
| `fs.config.perms_group_readable` | warn | Group users can read config tokens/settings | filesystem perms on config file | yes |
|
||||
| `fs.config.perms_world_readable` | critical | Config can expose tokens/settings | filesystem perms on config file | yes |
|
||||
| `fs.config_include.perms_writable` | critical | Config include file can be modified by others | include-file perms referenced from `openclaw.json` | yes |
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
|
||||
import { createConfigIO } from "./io.js";
|
||||
import {
|
||||
createConfigIO,
|
||||
resetConfigRuntimeState,
|
||||
setRuntimeConfigSnapshot,
|
||||
writeConfigFile,
|
||||
} from "./io.js";
|
||||
import type { ConfigFileSnapshot } from "./types.openclaw.js";
|
||||
|
||||
// Mock the plugin manifest registry so we can register a fake channel whose
|
||||
@@ -66,7 +71,12 @@ describe("config io write", () => {
|
||||
} satisfies PluginManifestRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetConfigRuntimeState();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
resetConfigRuntimeState();
|
||||
await suiteRootTracker.cleanup();
|
||||
});
|
||||
|
||||
@@ -428,4 +438,98 @@ describe("config io write", () => {
|
||||
plugins: [],
|
||||
} satisfies PluginManifestRegistry);
|
||||
});
|
||||
|
||||
it("writes runtime-derived edits back to source SecretRef markers", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
const previousConfigPath = process.env.OPENCLAW_CONFIG_PATH;
|
||||
process.env.OPENCLAW_CONFIG_PATH = configPath;
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
gateway: { mode: "local" },
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
try {
|
||||
setRuntimeConfigSnapshot(
|
||||
{
|
||||
gateway: { mode: "local" },
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: "sk-runtime-resolved",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { mode: "local" },
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await writeConfigFile({
|
||||
gateway: { mode: "local", port: 18789 },
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: "sk-runtime-resolved",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(JSON.parse(await fs.readFile(configPath, "utf-8"))).toEqual({
|
||||
gateway: { mode: "local", port: 18789 },
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
lastTouchedAt: expect.any(String),
|
||||
lastTouchedVersion: expect.any(String),
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
if (previousConfigPath === undefined) {
|
||||
delete process.env.OPENCLAW_CONFIG_PATH;
|
||||
} else {
|
||||
process.env.OPENCLAW_CONFIG_PATH = previousConfigPath;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user