feat: allow inline env vars in config
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Config: support inline env vars in config (`env.*` / `env.vars`) and document env precedence.
|
||||||
- Agent: enable adaptive context pruning by default for tool-result trimming.
|
- Agent: enable adaptive context pruning by default for tool-result trimming.
|
||||||
- Doctor: check config/state permissions and offer to tighten them. — thanks @steipete
|
- Doctor: check config/state permissions and offer to tighten them. — thanks @steipete
|
||||||
- Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete
|
- Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ and an **Auth overview** section showing which providers have profiles/env/model
|
|||||||
|
|
||||||
Input
|
Input
|
||||||
- OpenRouter `/models` list (filter `:free`)
|
- OpenRouter `/models` list (filter `:free`)
|
||||||
- Requires OpenRouter API key from auth profiles or `OPENROUTER_API_KEY`
|
- Requires OpenRouter API key from auth profiles or `OPENROUTER_API_KEY` (see [/environment](/environment))
|
||||||
- Optional filters: `--max-age-days`, `--min-params`, `--provider`, `--max-candidates`
|
- Optional filters: `--max-age-days`, `--min-params`, `--provider`, `--max-candidates`
|
||||||
- Probe controls: `--timeout`, `--concurrency`
|
- Probe controls: `--timeout`, `--concurrency`
|
||||||
|
|
||||||
|
|||||||
60
docs/environment.md
Normal file
60
docs/environment.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
summary: "Where Clawdbot loads environment variables and the precedence order"
|
||||||
|
read_when:
|
||||||
|
- You need to know which env vars are loaded, and in what order
|
||||||
|
- You are debugging missing API keys in the Gateway
|
||||||
|
- You are documenting provider auth or deployment environments
|
||||||
|
---
|
||||||
|
# Environment variables
|
||||||
|
|
||||||
|
Clawdbot pulls environment variables from multiple sources. The rule is **never override existing values**.
|
||||||
|
|
||||||
|
## Precedence (highest → lowest)
|
||||||
|
|
||||||
|
1) **Process environment** (what the Gateway process already has from the parent shell/daemon).
|
||||||
|
2) **`.env` in the current working directory** (dotenv default; does not override).
|
||||||
|
3) **Global `.env`** at `~/.clawdbot/.env` (aka `$CLAWDBOT_STATE_DIR/.env`; does not override).
|
||||||
|
4) **Config `env` block** in `~/.clawdbot/clawdbot.json` (applied only if missing).
|
||||||
|
5) **Optional login-shell import** (`env.shellEnv.enabled` or `CLAWDBOT_LOAD_SHELL_ENV=1`), applied only for missing expected keys.
|
||||||
|
|
||||||
|
If the config file is missing entirely, step 4 is skipped; shell import still runs if enabled.
|
||||||
|
|
||||||
|
## Config `env` block
|
||||||
|
|
||||||
|
Two equivalent ways to set inline env vars (both are non-overriding):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
OPENROUTER_API_KEY: "sk-or-...",
|
||||||
|
vars: {
|
||||||
|
GROQ_API_KEY: "gsk-..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shell env import
|
||||||
|
|
||||||
|
`env.shellEnv` runs your login shell and imports only **missing** expected keys:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
shellEnv: {
|
||||||
|
enabled: true,
|
||||||
|
timeoutMs: 15000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Env var equivalents:
|
||||||
|
- `CLAWDBOT_LOAD_SHELL_ENV=1`
|
||||||
|
- `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Gateway configuration](/gateway/configuration)
|
||||||
|
- [FAQ: env vars and .env loading](/start/faq#env-vars-and-env-loading)
|
||||||
|
- [Models overview](/concepts/models)
|
||||||
@@ -48,6 +48,10 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
|||||||
{
|
{
|
||||||
// Environment + shell
|
// Environment + shell
|
||||||
env: {
|
env: {
|
||||||
|
OPENROUTER_API_KEY: "sk-or-...",
|
||||||
|
vars: {
|
||||||
|
GROQ_API_KEY: "gsk-..."
|
||||||
|
},
|
||||||
shellEnv: {
|
shellEnv: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
timeoutMs: 15000
|
timeoutMs: 15000
|
||||||
|
|||||||
@@ -91,6 +91,22 @@ Additionally, it loads:
|
|||||||
|
|
||||||
Neither `.env` file overrides existing env vars.
|
Neither `.env` file overrides existing env vars.
|
||||||
|
|
||||||
|
You can also provide inline env vars in config. These are only applied if the
|
||||||
|
process env is missing the key (same non-overriding rule):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
OPENROUTER_API_KEY: "sk-or-...",
|
||||||
|
vars: {
|
||||||
|
GROQ_API_KEY: "gsk-..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See [/environment](/environment) for full precedence and sources.
|
||||||
|
|
||||||
### `env.shellEnv` (optional)
|
### `env.shellEnv` (optional)
|
||||||
|
|
||||||
Opt-in convenience: if enabled and none of the expected keys are set yet, CLAWDBOT runs your login shell and imports only the missing expected keys (never overrides).
|
Opt-in convenience: if enabled and none of the expected keys are set yet, CLAWDBOT runs your login shell and imports only the missing expected keys (never overrides).
|
||||||
@@ -1178,6 +1194,12 @@ Example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `agent.elevated` is **global** (not per-agent). Availability is based on sender allowlists.
|
||||||
|
- `/elevated on|off` stores state per session key; inline directives apply to a single message.
|
||||||
|
- Elevated `bash` runs on the host and bypasses sandboxing.
|
||||||
|
- Tool policy still applies; if `bash` is denied, elevated cannot be used.
|
||||||
|
|
||||||
`agent.maxConcurrent` sets the maximum number of embedded agent runs that can
|
`agent.maxConcurrent` sets the maximum number of embedded agent runs that can
|
||||||
execute in parallel across sessions. Each session is still serialized (one run
|
execute in parallel across sessions. Each session is still serialized (one run
|
||||||
per session key at a time). Default: 1.
|
per session key at a time). Default: 1.
|
||||||
|
|||||||
@@ -185,6 +185,19 @@ Clawdbot reads env vars from the parent process (shell, launchd/systemd, CI, etc
|
|||||||
|
|
||||||
Neither `.env` file overrides existing env vars.
|
Neither `.env` file overrides existing env vars.
|
||||||
|
|
||||||
|
You can also define inline env vars in config (applied only if missing from the process env):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
OPENROUTER_API_KEY: "sk-or-...",
|
||||||
|
vars: { GROQ_API_KEY: "gsk-..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See [/environment](/environment) for full precedence and sources.
|
||||||
|
|
||||||
### “I started the Gateway via a daemon and my env vars disappeared.” What now?
|
### “I started the Gateway via a daemon and my env vars disappeared.” What now?
|
||||||
|
|
||||||
Two common fixes:
|
Two common fixes:
|
||||||
|
|||||||
@@ -327,6 +327,80 @@ describe("config identity defaults", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("config env vars", () => {
|
||||||
|
it("applies env vars from env block when missing", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const configDir = path.join(home, ".clawdbot");
|
||||||
|
await fs.mkdir(configDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(configDir, "clawdbot.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
env: { OPENROUTER_API_KEY: "config-key" },
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
await withEnvOverride({ OPENROUTER_API_KEY: undefined }, async () => {
|
||||||
|
const { loadConfig } = await import("./config.js");
|
||||||
|
loadConfig();
|
||||||
|
expect(process.env.OPENROUTER_API_KEY).toBe("config-key");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not override existing env vars", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const configDir = path.join(home, ".clawdbot");
|
||||||
|
await fs.mkdir(configDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(configDir, "clawdbot.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
env: { OPENROUTER_API_KEY: "config-key" },
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
await withEnvOverride({ OPENROUTER_API_KEY: "existing-key" }, async () => {
|
||||||
|
const { loadConfig } = await import("./config.js");
|
||||||
|
loadConfig();
|
||||||
|
expect(process.env.OPENROUTER_API_KEY).toBe("existing-key");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies env vars from env.vars when missing", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const configDir = path.join(home, ".clawdbot");
|
||||||
|
await fs.mkdir(configDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(configDir, "clawdbot.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
env: { vars: { GROQ_API_KEY: "gsk-config" } },
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
await withEnvOverride({ GROQ_API_KEY: undefined }, async () => {
|
||||||
|
const { loadConfig } = await import("./config.js");
|
||||||
|
loadConfig();
|
||||||
|
expect(process.env.GROQ_API_KEY).toBe("gsk-config");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("config pruning defaults", () => {
|
describe("config pruning defaults", () => {
|
||||||
it("defaults contextPruning mode to adaptive", async () => {
|
it("defaults contextPruning mode to adaptive", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const SHELL_ENV_EXPECTED_KEYS = [
|
|||||||
"ANTHROPIC_OAUTH_TOKEN",
|
"ANTHROPIC_OAUTH_TOKEN",
|
||||||
"GEMINI_API_KEY",
|
"GEMINI_API_KEY",
|
||||||
"ZAI_API_KEY",
|
"ZAI_API_KEY",
|
||||||
|
"OPENROUTER_API_KEY",
|
||||||
"MINIMAX_API_KEY",
|
"MINIMAX_API_KEY",
|
||||||
"ELEVENLABS_API_KEY",
|
"ELEVENLABS_API_KEY",
|
||||||
"TELEGRAM_BOT_TOKEN",
|
"TELEGRAM_BOT_TOKEN",
|
||||||
@@ -78,6 +79,34 @@ function warnOnConfigMiskeys(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyConfigEnv(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
env: NodeJS.ProcessEnv,
|
||||||
|
): void {
|
||||||
|
const envConfig = cfg.env;
|
||||||
|
if (!envConfig) return;
|
||||||
|
|
||||||
|
const entries: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (envConfig.vars) {
|
||||||
|
for (const [key, value] of Object.entries(envConfig.vars)) {
|
||||||
|
if (!value) continue;
|
||||||
|
entries[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(envConfig)) {
|
||||||
|
if (key === "shellEnv" || key === "vars") continue;
|
||||||
|
if (typeof value !== "string" || !value.trim()) continue;
|
||||||
|
entries[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(entries)) {
|
||||||
|
if (env[key]?.trim()) continue;
|
||||||
|
env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveConfigPathForDeps(deps: Required<ConfigIoDeps>): string {
|
function resolveConfigPathForDeps(deps: Required<ConfigIoDeps>): string {
|
||||||
if (deps.configPath) return deps.configPath;
|
if (deps.configPath) return deps.configPath;
|
||||||
return resolveConfigPath(deps.env, resolveStateDir(deps.env, deps.homedir));
|
return resolveConfigPath(deps.env, resolveStateDir(deps.env, deps.homedir));
|
||||||
@@ -155,6 +184,8 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
|||||||
throw new DuplicateAgentDirError(duplicates);
|
throw new DuplicateAgentDirError(duplicates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyConfigEnv(cfg, deps.env);
|
||||||
|
|
||||||
const enabled =
|
const enabled =
|
||||||
shouldEnableShellEnvFallback(deps.env) ||
|
shouldEnableShellEnvFallback(deps.env) ||
|
||||||
cfg.env?.shellEnv?.enabled === true;
|
cfg.env?.shellEnv?.enabled === true;
|
||||||
|
|||||||
@@ -1031,6 +1031,14 @@ export type ClawdbotConfig = {
|
|||||||
/** Timeout for the login shell exec (ms). Default: 15000. */
|
/** Timeout for the login shell exec (ms). Default: 15000. */
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
/** Inline env vars to apply when not already present in the process env. */
|
||||||
|
vars?: Record<string, string>;
|
||||||
|
/** Sugar: allow env vars directly under env (string values only). */
|
||||||
|
[key: string]:
|
||||||
|
| string
|
||||||
|
| Record<string, string>
|
||||||
|
| { enabled?: boolean; timeoutMs?: number }
|
||||||
|
| undefined;
|
||||||
};
|
};
|
||||||
identity?: {
|
identity?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -793,7 +793,9 @@ export const ClawdbotSchema = z.object({
|
|||||||
timeoutMs: z.number().int().nonnegative().optional(),
|
timeoutMs: z.number().int().nonnegative().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
vars: z.record(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.catchall(z.string())
|
||||||
.optional(),
|
.optional(),
|
||||||
identity: z
|
identity: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user