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:
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user