fix: clean up Hermes-visible OpenClaw leftovers (#97)

* cleanup openclaw session leftovers - hermes can breathe now

* fix: load hermes adapter env from .env

* fix: redact secrets from hermes adapter error output

* addressed review findings

* address luke findings #2
This commit is contained in:
gsknnft
2026-04-03 18:35:13 -04:00
committed by GitHub
parent 051d0ce469
commit 4be98d7080
10 changed files with 404 additions and 45 deletions
+113 -18
View File
@@ -6,6 +6,9 @@ import {
defaultStudioSettings,
mergeStudioSettings,
normalizeStudioSettings,
type StudioGatewayAdapterType,
type StudioGatewayProfile,
type StudioGatewaySettings,
type StudioSettings,
type StudioSettingsPatch,
} from "@/lib/studio/settings";
@@ -16,6 +19,7 @@ import {
const SETTINGS_DIRNAME = "claw3d";
const SETTINGS_FILENAME = "settings.json";
const OPENCLAW_CONFIG_FILENAME = "openclaw.json";
const DEFAULT_LOCAL_GATEWAY_PORT = 18789;
export const resolveStudioSettingsPath = () =>
path.join(resolveStateDir(), SETTINGS_DIRNAME, SETTINGS_FILENAME);
@@ -23,11 +27,21 @@ export const resolveStudioSettingsPath = () =>
const isRecord = (value: unknown): value is Record<string, unknown> =>
Boolean(value && typeof value === "object");
const readOpenclawGatewayDefaults = (): {
const buildGatewaySettings = (params: {
adapterType: StudioGatewayAdapterType;
url: string;
token: string;
adapterType: "openclaw";
} | null => {
token?: string;
profiles?: Partial<Record<StudioGatewayAdapterType, StudioGatewayProfile>>;
}): StudioGatewaySettings => ({
url: params.url,
token: params.token ?? "",
adapterType: params.adapterType,
...(params.profiles ? { profiles: params.profiles } : {}),
});
const buildLocalProfile = (url: string, token = ""): StudioGatewayProfile => ({ url, token });
const readOpenclawGatewayDefaults = (): StudioGatewaySettings | null => {
try {
const configPath = path.join(resolveStateDir(), OPENCLAW_CONFIG_FILENAME);
if (!fs.existsSync(configPath)) return null;
@@ -40,29 +54,110 @@ const readOpenclawGatewayDefaults = (): {
const token = typeof auth?.token === "string" ? auth.token.trim() : "";
const port = typeof gateway.port === "number" && Number.isFinite(gateway.port) ? gateway.port : null;
if (!token) return null;
const url = port ? `ws://localhost:${port}` : "";
const url = port ? `ws://localhost:${port}` : `ws://localhost:${DEFAULT_LOCAL_GATEWAY_PORT}`;
if (!url) return null;
return { url, token, adapterType: "openclaw" };
return buildGatewaySettings({
adapterType: "openclaw",
url,
token,
profiles: {
openclaw: buildLocalProfile(url, token),
},
});
} catch {
return null;
}
};
export const loadLocalGatewayDefaults = (): {
url: string;
token: string;
adapterType: "openclaw";
} | null => {
const fromFile = readOpenclawGatewayDefaults();
if (fromFile) return fromFile;
// Fall back to env vars so operators can configure the gateway URL at
// runtime without openclaw.json and without a rebuild.
const envUrl = process.env.CLAW3D_GATEWAY_URL?.trim();
const envToken = process.env.CLAW3D_GATEWAY_TOKEN?.trim();
if (envUrl) return { url: envUrl, token: envToken ?? "", adapterType: "openclaw" };
const normalizeAdapterType = (value: string | undefined): StudioGatewayAdapterType | null => {
const normalized = value?.trim().toLowerCase();
if (normalized === "openclaw" || normalized === "hermes" || normalized === "demo" || normalized === "custom") {
return normalized;
}
return null;
};
const readPortBasedGatewayProfile = (
adapterType: Extract<StudioGatewayAdapterType, "hermes" | "demo">,
envKey: "HERMES_ADAPTER_PORT" | "DEMO_ADAPTER_PORT"
): StudioGatewayProfile | null => {
const rawPort = process.env[envKey]?.trim();
if (!rawPort) return null;
const port = Number.parseInt(rawPort, 10);
if (!Number.isFinite(port) || port <= 0) return null;
return buildLocalProfile(`ws://localhost:${port}`);
};
const buildEnvGatewayDefaults = (): StudioGatewaySettings | null => {
const envUrl = process.env.CLAW3D_GATEWAY_URL?.trim();
const envToken = process.env.CLAW3D_GATEWAY_TOKEN?.trim() ?? "";
const envAdapterType =
normalizeAdapterType(process.env.CLAW3D_GATEWAY_ADAPTER_TYPE) ?? "openclaw";
const hermesProfile = readPortBasedGatewayProfile("hermes", "HERMES_ADAPTER_PORT");
const demoProfile = readPortBasedGatewayProfile("demo", "DEMO_ADAPTER_PORT");
const profiles: Partial<Record<StudioGatewayAdapterType, StudioGatewayProfile>> = {};
if (hermesProfile) profiles.hermes = hermesProfile;
if (demoProfile) profiles.demo = demoProfile;
if (envUrl) {
profiles[envAdapterType] = buildLocalProfile(envUrl, envToken);
return buildGatewaySettings({
adapterType: envAdapterType,
url: envUrl,
token: envToken,
profiles,
});
}
const fallbackProfile = profiles.hermes ?? profiles.demo ?? null;
if (!fallbackProfile) return null;
const fallbackAdapterType = profiles.hermes ? "hermes" : "demo";
return buildGatewaySettings({
adapterType: fallbackAdapterType,
url: fallbackProfile.url,
token: fallbackProfile.token,
profiles,
});
};
const mergeGatewayProfiles = (
base: StudioGatewaySettings,
extra: StudioGatewaySettings | null
): StudioGatewaySettings => {
if (!extra?.profiles) {
return base;
}
const mergedProfiles: Partial<Record<StudioGatewayAdapterType, StudioGatewayProfile>> = {
...(base.profiles ?? {}),
};
for (const [adapterType, profile] of Object.entries(extra.profiles) as Array<
[StudioGatewayAdapterType, StudioGatewayProfile | undefined]
>) {
if (!profile || mergedProfiles[adapterType]) {
continue;
}
mergedProfiles[adapterType] = profile;
}
return {
...base,
profiles: mergedProfiles,
};
};
export const loadLocalGatewayDefaults = (): StudioGatewaySettings | null => {
const fromFile = readOpenclawGatewayDefaults();
const fromEnv = buildEnvGatewayDefaults();
if (fromFile) {
return mergeGatewayProfiles(fromFile, fromEnv);
}
// Fall back to env vars so operators can configure the gateway URL at
// runtime without openclaw.json and without a rebuild. If no explicit
// URL is provided, also expose local Hermes/Demo adapter ports when set.
return fromEnv;
};
export const loadStudioSettings = (): StudioSettings => {
const settingsPath = resolveStudioSettingsPath();
if (!fs.existsSync(settingsPath)) {