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
@@ -25,6 +25,7 @@ import type { AgentState } from "@/features/agents/state/store";
import type { CronCreateDraft, CronCreateTemplateId } from "@/lib/cron/createPayloadBuilder";
import { formatCronPayload, formatCronSchedule, type CronJobSummary } from "@/lib/cron/types";
import type { SkillStatusReport } from "@/lib/skills/types";
import type { StudioGatewayAdapterType } from "@/lib/studio/settings";
export type AgentSettingsPanelProps = {
agent: AgentState;
@@ -47,6 +48,7 @@ export type AgentSettingsPanelProps = {
cronCreateBusy?: boolean;
onCreateCronJob?: (draft: CronCreateDraft) => Promise<void> | void;
controlUiUrl?: string | null;
adapterType?: StudioGatewayAdapterType | null;
skillsReport?: SkillStatusReport | null;
skillsLoading?: boolean;
skillsError?: string | null;
@@ -248,6 +250,7 @@ export const AgentSettingsPanel = ({
cronCreateBusy = false,
onCreateCronJob = () => {},
controlUiUrl = null,
adapterType = "openclaw",
skillsReport = null,
skillsLoading = false,
skillsError = null,
@@ -267,6 +270,7 @@ export const AgentSettingsPanel = ({
onSkillApiKeyChange = () => {},
onSaveSkillApiKey = () => {},
}: AgentSettingsPanelProps) => {
const isOpenClawRuntime = adapterType === "openclaw";
const initialPermissionsDraft =
permissionsDraft ?? resolvePresetDefaultsForRole(resolveExecutionRoleFromAgent(agent));
const [permissionsBaselineValue, setPermissionsBaselineValue] =
@@ -785,16 +789,18 @@ export const AgentSettingsPanel = ({
})}
</div>
) : null}
<section className="sidebar-section" data-testid="agent-settings-heartbeat-coming-soon">
<h3 className="sidebar-section-title">Heartbeats</h3>
<div className="mt-3 text-[11px] text-muted-foreground">
Heartbeat automation controls are coming soon.
</div>
</section>
{isOpenClawRuntime ? (
<section className="sidebar-section" data-testid="agent-settings-heartbeat-coming-soon">
<h3 className="sidebar-section-title">Heartbeats</h3>
<div className="mt-3 text-[11px] text-muted-foreground">
Heartbeat automation controls are coming soon.
</div>
</section>
) : null}
</section>
) : null}
{mode === "advanced" ? (
{mode === "advanced" && isOpenClawRuntime ? (
<>
<section className="sidebar-section mt-8" data-testid="agent-settings-control-ui">
<h3 className="sidebar-section-title ui-text-danger">Danger Zone</h3>
@@ -1691,7 +1691,8 @@ const AgentsPageScreen = () => {
onDeleteCronJob={(jobId) =>
settingsMutationController.handleDeleteCronJob(inspectSidebarAgent.agentId, jobId)
}
controlUiUrl={controlUiUrl}
controlUiUrl={selectedAdapterType === "openclaw" ? controlUiUrl : null}
adapterType={selectedAdapterType}
/>
</div>
</div>
+2 -1
View File
@@ -727,7 +727,8 @@ export const useGatewayConnection = (
}
: null;
// When the user has no saved gateway URL, prefer the runtime
// localGatewayDefaults (from openclaw.json / CLAW3D_GATEWAY_URL)
// localGatewayDefaults (from openclaw.json, CLAW3D_GATEWAY_URL,
// or detected local Hermes/demo adapter ports)
// over the build-time NEXT_PUBLIC_GATEWAY_URL which may be stale
// or empty if the operator forgot to rebuild after .env changes.
const hasSavedUrl = Boolean(gateway?.url?.trim());
+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)) {