[codex] scope bridge terminal env refresh to worker startup (#1031)
* fix(bridge): refresh terminal env from profile config on profile switch Profile switching changes HERMES_HOME but the TERMINAL_* environment variables (TERMINAL_ENV, TERMINAL_SSH_HOST, etc.) still point to the root config's terminal settings set at gateway startup. Add _refresh_terminal_env() that re-reads terminal config from the active profile's config.yaml and sets the corresponding TERMINAL_* env vars. Call it in: - AgentPool.get_or_create(): when creating a session for a profile - AgentPool._run_chat(): before agent execution, inside _profile_env Errors are handled gracefully: YAML parse failures log to stderr, terminal_tool cache invalidation failures are silently ignored, and missing config files are skipped without error. * fix(bridge): refresh terminal env in worker profile setup _set_worker_profile_env() handles broker-spawned worker subprocesses that are isolated per profile. The worker inherits TERMINAL_* env vars from the broker (root config), and _profile_env() is a no-op in worker mode, so terminal config was never refreshed for non-default profiles. Adding _refresh_terminal_env() here means each worker subprocess reads its own profile's config.yaml terminal section on startup, solving profile-isolated terminal backends (e.g. SSH per profile). * fix bridge terminal env refresh scope * refresh worker profile env for new agents * avoid bridge worker restart for channel config * align config controller bridge restart tests --------- Co-authored-by: GoldenFish123321 <goldfishx@gmail.com> Co-authored-by: GoldenFishX <golden_fish@foxmail.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { readFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { getActiveProfileName, getProfileDir } from '../../services/hermes/hermes-profile'
|
||||
import { AgentBridgeClient } from '../../services/hermes/agent-bridge'
|
||||
import { restartGatewayForProfile } from '../../services/hermes/gateway-autostart'
|
||||
import { saveEnvValueForProfile } from '../../services/config-helpers'
|
||||
import { logger } from '../../services/logger'
|
||||
@@ -84,15 +83,6 @@ function deepMerge(target: Record<string, any>, source: Record<string, any>): Re
|
||||
return target
|
||||
}
|
||||
|
||||
async function destroyBridgeProfile(profile: string): Promise<void> {
|
||||
try {
|
||||
const result = await new AgentBridgeClient({ connectRetryMs: 0, timeoutMs: 5000 }).destroyProfile(profile)
|
||||
logger.info('[config] destroyed bridge sessions after gateway restart profile=%s destroyed=%s', profile, result.destroyed)
|
||||
} catch (err) {
|
||||
logger.warn(err, '[config] failed to destroy bridge sessions after gateway restart profile=%s', profile)
|
||||
}
|
||||
}
|
||||
|
||||
async function readEnvPlatforms(profile: string): Promise<Record<string, any>> {
|
||||
try {
|
||||
const raw = await readFile(envPath(profile), 'utf-8')
|
||||
@@ -159,13 +149,12 @@ export async function updateConfig(ctx: any) {
|
||||
},
|
||||
})
|
||||
|
||||
// Platform adapters still run through Hermes gateway; restart it so channel
|
||||
// config changes (Feishu/Weixin/etc.) are applied, then refresh bridge sessions.
|
||||
// Platform adapters run through Hermes gateway; restart it so channel
|
||||
// config changes (Feishu/Weixin/etc.) are applied.
|
||||
if (restart !== false && PLATFORM_SECTIONS.has(section)) {
|
||||
try {
|
||||
const restartResult = await restartGatewayForProfile(profile)
|
||||
logger.info('[config] gateway restarted after config update section=%s profile=%s result=%j', section, profile, restartResult)
|
||||
await destroyBridgeProfile(profile)
|
||||
} catch (err) {
|
||||
logger.error(err, 'Gateway restart failed')
|
||||
ctx.status = 500
|
||||
@@ -227,12 +216,11 @@ export async function updateCredentials(ctx: any) {
|
||||
},
|
||||
})
|
||||
|
||||
// Platform adapters still run through Hermes gateway; restart it so channel
|
||||
// credentials are applied, then refresh bridge sessions.
|
||||
// Platform adapters run through Hermes gateway; restart it so channel
|
||||
// credentials are applied.
|
||||
try {
|
||||
const restartResult = await restartGatewayForProfile(profile)
|
||||
logger.info('[config] gateway restarted after credentials update platform=%s profile=%s result=%j', platform, profile, restartResult)
|
||||
await destroyBridgeProfile(profile)
|
||||
} catch (err) {
|
||||
logger.error(err, 'Gateway restart failed')
|
||||
ctx.status = 500
|
||||
|
||||
@@ -426,9 +426,20 @@ def _set_worker_profile_env(profile: str | None) -> None:
|
||||
profile_home = _profile_home(profile)
|
||||
os.environ["HERMES_HOME"] = str(profile_home)
|
||||
os.environ["HERMES_AGENT_BRIDGE_WORKER_PROFILE"] = profile or "default"
|
||||
_refresh_worker_profile_env()
|
||||
|
||||
|
||||
def _refresh_worker_profile_env() -> None:
|
||||
"""Overlay the current worker profile .env/config before creating a new agent."""
|
||||
profile = _worker_profile()
|
||||
if not profile:
|
||||
return
|
||||
profile_home = _profile_home(profile)
|
||||
os.environ["HERMES_HOME"] = str(profile_home)
|
||||
values = _read_dotenv(profile_home / ".env")
|
||||
for key, value in values.items():
|
||||
os.environ[key] = value
|
||||
_refresh_terminal_env()
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -445,6 +456,71 @@ def _profile_env(profile: str | None):
|
||||
_restore_profile_env(original)
|
||||
|
||||
|
||||
def _refresh_terminal_env() -> None:
|
||||
"""Bridge current worker HERMES_HOME/config.yaml terminal config to TERMINAL_* env vars.
|
||||
|
||||
Worker startup first overlays the profile .env, then this function lets
|
||||
terminal config.yaml values override the matching terminal environment vars.
|
||||
"""
|
||||
hermes_home = os.environ.get("HERMES_HOME", "")
|
||||
if not hermes_home:
|
||||
return
|
||||
config_path = Path(hermes_home) / "config.yaml"
|
||||
if not config_path.exists():
|
||||
return
|
||||
try:
|
||||
import yaml
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
terminal_cfg = cfg.get("terminal", {})
|
||||
if not isinstance(terminal_cfg, dict):
|
||||
return
|
||||
TERMINAL_ENV_MAP = {
|
||||
"backend": "TERMINAL_ENV",
|
||||
"cwd": "TERMINAL_CWD",
|
||||
"timeout": "TERMINAL_TIMEOUT",
|
||||
"lifetime_seconds": "TERMINAL_LIFETIME_SECONDS",
|
||||
"ssh_host": "TERMINAL_SSH_HOST",
|
||||
"ssh_user": "TERMINAL_SSH_USER",
|
||||
"ssh_port": "TERMINAL_SSH_PORT",
|
||||
"ssh_key": "TERMINAL_SSH_KEY",
|
||||
"docker_image": "TERMINAL_DOCKER_IMAGE",
|
||||
"docker_forward_env": "TERMINAL_DOCKER_FORWARD_ENV",
|
||||
"singularity_image": "TERMINAL_SINGULARITY_IMAGE",
|
||||
"modal_image": "TERMINAL_MODAL_IMAGE",
|
||||
"daytona_image": "TERMINAL_DAYTONA_IMAGE",
|
||||
"vercel_runtime": "TERMINAL_VERCEL_RUNTIME",
|
||||
"container_cpu": "TERMINAL_CONTAINER_CPU",
|
||||
"container_memory": "TERMINAL_CONTAINER_MEMORY",
|
||||
"container_disk": "TERMINAL_CONTAINER_DISK",
|
||||
"container_persistent": "TERMINAL_CONTAINER_PERSISTENT",
|
||||
"docker_volumes": "TERMINAL_DOCKER_VOLUMES",
|
||||
"docker_env": "TERMINAL_DOCKER_ENV",
|
||||
"docker_mount_cwd_to_workspace": "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE",
|
||||
"docker_run_as_host_user": "TERMINAL_DOCKER_RUN_AS_HOST_USER",
|
||||
"sandbox_dir": "TERMINAL_SANDBOX_DIR",
|
||||
"persistent_shell": "TERMINAL_PERSISTENT_SHELL",
|
||||
"modal_mode": "TERMINAL_MODAL_MODE",
|
||||
}
|
||||
for cfg_key, env_var in TERMINAL_ENV_MAP.items():
|
||||
if cfg_key in terminal_cfg:
|
||||
val = terminal_cfg[cfg_key]
|
||||
if cfg_key == "cwd" and str(val) in {".", "auto", "cwd"}:
|
||||
continue
|
||||
if cfg_key == "cwd" and isinstance(val, str):
|
||||
val = os.path.expanduser(val)
|
||||
if isinstance(val, (list, dict)):
|
||||
os.environ[env_var] = json.dumps(val)
|
||||
else:
|
||||
os.environ[env_var] = str(val)
|
||||
except Exception:
|
||||
print(
|
||||
f"[hermes-bridge] Failed to refresh terminal env from {config_path}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_model(cfg: dict[str, Any]) -> str:
|
||||
env_model = (
|
||||
os.environ.get("HERMES_MODEL", "")
|
||||
@@ -637,6 +713,7 @@ class AgentPool:
|
||||
from run_agent import AIAgent
|
||||
|
||||
with _profile_env(profile):
|
||||
_refresh_worker_profile_env()
|
||||
cfg = _load_cfg()
|
||||
resolved_model = requested_model or _resolve_model(cfg)
|
||||
runtime = _resolve_runtime(resolved_model, requested_provider or None)
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('config controller locked file updates', () => {
|
||||
|
||||
expect(ctx.body).toEqual({ success: true })
|
||||
expect(mockRestartGateway).toHaveBeenCalledWith('default')
|
||||
expect(mockDestroyProfile).toHaveBeenCalledWith('default')
|
||||
expect(mockDestroyProfile).not.toHaveBeenCalled()
|
||||
const config = YAML.load(await readFile(join(hermesHome, 'config.yaml'), 'utf-8')) as any
|
||||
expect(config.telegram.enabled).toBe(true)
|
||||
expect(config.telegram.extra).toEqual({ mode: 'old', token_mode: 'env' })
|
||||
@@ -191,7 +191,7 @@ describe('config controller locked file updates', () => {
|
||||
}, 'research'))
|
||||
|
||||
expect(mockRestartGateway).toHaveBeenCalledWith('research')
|
||||
expect(mockDestroyProfile).toHaveBeenCalledWith('research')
|
||||
expect(mockDestroyProfile).not.toHaveBeenCalled()
|
||||
const defaultConfig = YAML.load(await readFile(join(hermesHome, 'config.yaml'), 'utf-8')) as any
|
||||
const researchConfig = YAML.load(await readFile(join(researchDir, 'config.yaml'), 'utf-8')) as any
|
||||
expect(defaultConfig.telegram.require_mention).toBe(false)
|
||||
|
||||
Reference in New Issue
Block a user