Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 19 additions & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,25 @@ Gemini / OpenCode / Antigravity / GitHub Copilot), with no MCP protocol support

---

### Per-Bot Environment Variables

Each `bots.json` entry can define its own `env` object. These variables are injected into that bot's daemon/CLI process, which is useful for per-bot proxies, API base URLs, or CLI-specific feature flags:

```json
{
"cliId": "codex",
"workingDir": "~/projects",
"env": {
"HTTPS_PROXY": "http://127.0.0.1:7890",
"OPENAI_BASE_URL": "https://api.example.com/v1"
}
}
```

`env` only accepts valid environment variable names, with string, number, or boolean values. Do not treat it as a secret vault: process environments may be visible to local diagnostic tools.

---

## 📖 Documentation

The full reference — commands, config, best practices, troubleshooting — lives in the docs site; not duplicated here —
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,25 @@ PersonalAgent 默认配好事件订阅 + bot 能力,正常情况下不用动

---

### Bot 级环境变量

每个 `bots.json` 条目都可以配置独立的 `env` 对象,用于给对应 bot 的 daemon/CLI 进程注入环境变量,例如代理、API base URL 或 CLI 专属开关:

```json
{
"cliId": "codex",
"workingDir": "~/projects",
"env": {
"HTTPS_PROXY": "http://127.0.0.1:7890",
"OPENAI_BASE_URL": "https://api.example.com/v1"
}
}
```

`env` 只接受合法环境变量名,值支持字符串、数字、布尔值;不要把它当成安全密钥库,进程环境可能被本机诊断工具看到。

---

## 📖 完整文档

命令、配置、最佳实践、排错的完整内容都在文档站,这里不再重复 ——
Expand Down
6 changes: 5 additions & 1 deletion bots.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
"allowedUsers": ["alice@company.com"],
"allowedChatGroups": ["oc_xxx_team"],
"workingDir": "~/projects",
"customPassthroughCommands": ["/goal", "/export"]
"customPassthroughCommands": ["/goal", "/export"],
"env": {
"HTTPS_PROXY": "http://127.0.0.1:7890",
"OPENAI_BASE_URL": "https://api.example.com/v1"
}
},
{
"larkAppId": "cli_xxx_bot2",
Expand Down
2 changes: 2 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { writeBotsJsonAtomic as writeBotsAtomic } from './setup/bots-store.js';
import {
applyBotConfigEdits,
assertUniqueBotProcessNames,
botProcessEnv,
botProcessName,
normalizeBotConfig,
parseBotConfigsJson,
Expand Down Expand Up @@ -254,6 +255,7 @@ function ecosystemConfig(): string {
error_file: join(LOG_DIR, `daemon-${i}-error.log`),
out_file: join(LOG_DIR, `daemon-${i}-out.log`),
env: {
...botProcessEnv(_bot),
SESSION_DATA_DIR: DATA_DIR,
BOTMUX_BOT_INDEX: String(i),
// Native-memory diagnostics. Default off; operator can flip it on
Expand Down
13 changes: 13 additions & 0 deletions src/setup/bot-config-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,19 @@ export function botProcessName(
return `${prefix}-${name ?? index}`;
}

export function botProcessEnv(bot: { env?: unknown }): Record<string, string> {
const raw = bot && typeof bot === 'object' && bot.env && typeof bot.env === 'object' && !Array.isArray(bot.env)
? bot.env as Record<string, unknown>
: {};
const out: Record<string, string> = {};
for (const [key, value] of Object.entries(raw)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof value === 'string') out[key] = value;
else if (typeof value === 'number' || typeof value === 'boolean') out[key] = String(value);
}
return out;
}

export function normalizeBotConfig<T extends Record<string, any>>(bot: T): T {
const out: Record<string, any> = { ...bot };
if (typeof out.name !== 'string') return out as T;
Expand Down
38 changes: 38 additions & 0 deletions test/bot-config-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
applyBotConfigEdits,
assertUniqueBotProcessNames,
assertOwnerWhenChatGroups,
botProcessEnv,
botProcessName,
findInvalidAllowedUserEntries,
hasOwnerEntry,
Expand Down Expand Up @@ -222,6 +223,43 @@ describe('applyBotConfigEdits', () => {
});
});

describe('botProcessEnv', () => {
it('keeps valid process env keys and stringifies primitive values', () => {
expect(botProcessEnv({
env: {
HTTPS_PROXY: 'http://127.0.0.1:7890',
OPENAI_TIMEOUT_MS: 30000,
FEATURE_FLAG: true,
EMPTY_VALUE: '',
},
})).toEqual({
HTTPS_PROXY: 'http://127.0.0.1:7890',
OPENAI_TIMEOUT_MS: '30000',
FEATURE_FLAG: 'true',
EMPTY_VALUE: '',
});
});

it('drops invalid keys and non-primitive values', () => {
expect(botProcessEnv({
env: {
'1BAD': 'x',
'BAD-NAME': 'x',
OK_NAME: ['x'],
ALSO_OK: { nested: true },
NULLISH: null,
VALID_NAME: false,
},
})).toEqual({ VALID_NAME: 'false' });
});

it('returns an empty object when env is missing or not an object', () => {
expect(botProcessEnv({})).toEqual({});
expect(botProcessEnv({ env: [] })).toEqual({});
expect(botProcessEnv({ env: 'HTTPS_PROXY=x' })).toEqual({});
});
});

describe('resolveCliId', () => {
it('returns undefined for empty input so callers can preserve current cliId', () => {
expect(resolveCliId('')).toBeUndefined();
Expand Down