Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,50 @@ describe("Hermes Portable connect recovery errors", () => {
expect(output).not.toContain("Hermes Portable inference recovery for 'alpha' failed");
});

it("verifies a compatible-endpoint route without Ollama recovery", async () => {
const entry = {
name: "alpha",
agent: "hermes",
provider: "compatible-endpoint",
model: "descriptor/model",
policies: [],
openshellDriver: "docker",
gatewayName: "nemoclaw",
lifecycleGeneration: "generation-1",
} as never;
const harness = createConnectHarness({
agentName: "hermes",
sessionAgent: { name: "hermes" },
registryEntry: entry,
inferenceGetOutput:
"Gateway inference:\n Provider: compatible-endpoint\n Model: descriptor/model\n",
inferenceProbeResponses: ["OK 200"],
portableReceiptDisposition: { kind: "hermes", phase: "active" },
portableRecoveryResult: { kind: "already-running" },
readinessDecision: {
kind: "accepted",
category: "accepted",
agent: { name: "hermes" },
sb: entry,
},
});

await expect(harness.connectSandbox("alpha", { probeOnly: true })).resolves.toBeUndefined();

expect(harness.registryEntries[0]?.hostLocalInferenceReceipt).toBeUndefined();
expect(harness.recoverHermesPortableOllamaInferenceSpy).not.toHaveBeenCalled();
expect(harness.captureResolvedOpenshellSpy).toHaveBeenCalledWith(
["inference", "get", "-g", "nemoclaw"],
expect.objectContaining({ openshellBinary: "/usr/bin/openshell" }),
);
expect(
harness.captureResolvedOpenshellSpy.mock.calls.some(
([args]) => Array.isArray(args) && args[0] === "sandbox" && args[1] === "exec",
),
).toBe(true);
expect(harness.publishLaunchReadinessSpy).not.toHaveBeenCalled();
});

it.each([
[
"authority drift",
Expand Down
172 changes: 128 additions & 44 deletions src/lib/actions/sandbox/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ import {
HermesPortableForwardRecoveryError,
type HermesPortableForwardRecoveryFailure,
type ManagedGatewayControlCompletion,
recoverHermesPortableLaunchForwards,
prepareHermesPortableLaunchForwards,
type PreparedHermesPortableForwardRecovery,
resolveSandboxDashboardPort,
resolveSandboxLaunchForwardPorts,
waitForManagedGatewaySupervisor,
Expand Down Expand Up @@ -330,40 +331,26 @@ async function runSandboxConnectProbe(
const agentName = agentRuntime.getAgentDisplayName(agent);
if (hermesPortable) {
if (probeOnly !== true) throw new Error("Hermes inference recovery requires probe-only mode");
measure("inference", () =>
recoverHermesPortableInferenceRouteForProbeOnlyOrExit(sandboxName, agent),
const route = measure("inference", () =>
verifyOrRecoverHermesPortableInferenceRouteForProbeOnlyOrExit(sandboxName, agent, undefined, {
probeTiming,
}),
);
let authority: HermesPortableActiveLifecycleAuthority;
try {
authority = requireHermesPortableActiveLifecycleAuthority(
sandboxName,
undefined,
portableAgentLifecycleAuthorityDeps(),
);
} catch {
probeTiming?.setForwardAction("failed");
probeTiming?.markFailureStage("forward");
failHermesPortableForwardRecovery(sandboxName, "authority-drift");
}
let forwardRecovery: ReturnType<typeof recoverHermesPortableForwardsForConnectProbe>;
try {
forwardRecovery = measure("forward", () =>
recoverHermesPortableForwardsForConnectProbe({
intent: "connect-probe-only",
if (!route.forwardsRecovered) {
let authority: HermesPortableActiveLifecycleAuthority;
try {
authority = requireHermesPortableActiveLifecycleAuthority(
sandboxName,
authority,
readRegistry: registry.getSandbox,
}),
);
} catch (error) {
probeTiming?.setForwardAction("failed");
probeTiming?.markFailureStage("forward");
failHermesPortableForwardRecovery(
sandboxName,
error instanceof HermesPortableForwardRecoveryError ? error.failure : "recovery-failed",
);
undefined,
portableAgentLifecycleAuthorityDeps(),
);
} catch {
probeTiming?.setForwardAction("failed");
probeTiming?.markFailureStage("forward");
failHermesPortableForwardRecovery(sandboxName, "authority-drift");
}
recoverHermesPortableForwardsForConnectProbeOrExit(sandboxName, authority, probeTiming);
}
probeTiming?.setForwardAction(forwardRecovery.kind === "restored" ? "restored" : "verified");
console.log(
` Probe complete: ${agentName} passed receipt-owned authenticated health in '${sandboxName}'.`,
);
Expand Down Expand Up @@ -566,7 +553,7 @@ type HermesPortableForwardConnectRecoveryInput = {
};

/** Restore the launch-readiness forwards through current Hermes command authority. */
function recoverHermesPortableForwardsForConnectProbe(
function prepareHermesPortableForwardsForConnectProbe(
input: HermesPortableForwardConnectRecoveryInput,
) {
const expectedEntry = structuredClone(input.authority.entry);
Expand Down Expand Up @@ -623,7 +610,7 @@ function recoverHermesPortableForwardsForConnectProbe(
});
};

return recoverHermesPortableLaunchForwards({
return prepareHermesPortableLaunchForwards({
intent: input.intent,
sandboxName: input.sandboxName,
gatewayName: input.authority.gatewayName,
Expand All @@ -639,6 +626,50 @@ function recoverHermesPortableForwardsForConnectProbe(
});
}

function recoverHermesPortableForwardsForConnectProbeOrExit(
sandboxName: string,
authority: HermesPortableActiveLifecycleAuthority,
probeTiming?: ProbeTimingRecorder,
): void {
try {
const prepared = prepareHermesPortableForwardsForConnectProbeMeasured(
sandboxName,
authority,
probeTiming,
);
const forwardRecovery = prepared.release();
probeTiming?.setForwardAction(forwardRecovery.kind === "restored" ? "restored" : "verified");
} catch (error) {
failHermesPortableForwardRecovery(
sandboxName,
error instanceof HermesPortableForwardRecoveryError ? error.failure : "recovery-failed",
);
}
}

function prepareHermesPortableForwardsForConnectProbeMeasured(
sandboxName: string,
authority: HermesPortableActiveLifecycleAuthority,
probeTiming?: ProbeTimingRecorder,
) {
try {
const prepare = () =>
prepareHermesPortableForwardsForConnectProbe({
intent: "connect-probe-only",
sandboxName,
authority,
readRegistry: registry.getSandbox,
});
return probeTiming ? probeTiming.measure("forward", prepare) : prepare();
} catch (error) {
probeTiming?.setForwardAction("failed");
probeTiming?.markFailureStage("forward");
throw error instanceof HermesPortableForwardRecoveryError
? error
: new HermesPortableForwardRecoveryError("recovery-failed");
}
}

class HermesPortableInferenceRouteVerificationError extends Error {
constructor(readonly reason: string) {
super("Hermes Portable inference route verification failed");
Expand Down Expand Up @@ -743,12 +774,22 @@ function verifyHermesPortableInferenceRouteOrExit(
}
}

/** Resume published Ollama authority only for the explicit probe-only command. */
function recoverHermesPortableInferenceRouteForProbeOnlyOrExit(
type HermesPortableProbeRouteResult = {
readonly forwardsRecovered: boolean;
};

type HermesPortableProbeRouteOptions = {
readonly probeTiming?: ProbeTimingRecorder;
readonly validateVerified?: (entry: SandboxEntry) => void;
};

/** Verify the recorded route and resume published Ollama only for probe-only recovery. */
function verifyOrRecoverHermesPortableInferenceRouteForProbeOnlyOrExit(
sandboxName: string,
agent: InferenceRouteProbeAgent,
expectedAuthority?: HermesPortableActiveLifecycleAuthority,
): SandboxEntry {
options: HermesPortableProbeRouteOptions = {},
): HermesPortableProbeRouteResult {
let authority: HermesPortableActiveLifecycleAuthority;
try {
authority = requireHermesPortableActiveLifecycleAuthority(
Expand All @@ -759,18 +800,44 @@ function recoverHermesPortableInferenceRouteForProbeOnlyOrExit(
} catch {
failHermesPortableInferenceRoute(sandboxName, "missing or incomplete");
}
if (authority.entry.provider !== "ollama-local") {
const entry = verifyHermesPortableInferenceRouteOrExit(sandboxName, agent, authority);
try {
options.validateVerified?.(entry);
} catch {
failHermesPortableInferenceRoute(sandboxName, "changed during verification");
}
return { forwardsRecovered: false };
}
let verified: SandboxEntry | null = null;
let preparedForwards: PreparedHermesPortableForwardRecovery | null = null;
try {
recoverHermesPortableInferenceForConnectProbe({
sandboxName,
authority,
readRegistry: registry.getSandbox,
verifyRoute: () => {
verified = verifyHermesPortableInferenceRoute(sandboxName, agent, authority);
try {
options.validateVerified?.(verified);
} catch {
refuseHermesPortableInferenceRoute("changed during verification");
}
return verified;
},
prepareProbeDependency: () => {
preparedForwards = prepareHermesPortableForwardsForConnectProbeMeasured(
sandboxName,
authority,
options.probeTiming,
);
return preparedForwards;
},
});
} catch (error) {
if (error instanceof HermesPortableForwardRecoveryError) {
failHermesPortableForwardRecovery(sandboxName, error.failure);
}
if (error instanceof HermesPortableInferenceRouteVerificationError) {
failHermesPortableInferenceRoute(sandboxName, error.reason);
}
Expand All @@ -779,8 +846,14 @@ function recoverHermesPortableInferenceRouteForProbeOnlyOrExit(
classifyHermesPortableInferenceConnectRecoveryFailure(error),
);
}
if (!verified) failHermesPortableInferenceRoute(sandboxName, "unreachable");
return verified;
const retainedForwards = preparedForwards as PreparedHermesPortableForwardRecovery | null;
if (!verified || !retainedForwards) {
failHermesPortableInferenceRoute(sandboxName, "unreachable");
}
options.probeTiming?.setForwardAction(
retainedForwards.result.kind === "restored" ? "restored" : "verified",
);
return { forwardsRecovered: true };
}

const GATEWAY_UNAVAILABLE_RE =
Expand Down Expand Up @@ -2015,16 +2088,27 @@ async function prepareConnectSandboxWithinLifecycleFence(
if (probeOnly !== true) {
throw new Error("Hermes inference recovery requires probe-only mode");
}
const verified = probeTiming!.measure("inference", () =>
recoverHermesPortableInferenceRouteForProbeOnlyOrExit(
const route = probeTiming!.measure("inference", () =>
verifyOrRecoverHermesPortableInferenceRouteForProbeOnlyOrExit(
sandboxName,
acceptedReadiness.agent,
activeAuthority,
{
probeTiming,
validateVerified: (verified) =>
probeTiming!.measure("authority", () =>
assertHermesPortableLifecycleForConnect(sandboxName, verified, gatewayName),
),
},
),
);
probeTiming!.measure("authority", () =>
assertHermesPortableLifecycleForConnect(sandboxName, verified, gatewayName),
);
if (!route.forwardsRecovered) {
recoverHermesPortableForwardsForConnectProbeOrExit(
sandboxName,
activeAuthority,
probeTiming,
);
}
}
console.log(` Probe complete: launch readiness is healthy for '${sandboxName}'.`);
return null;
Expand Down
7 changes: 6 additions & 1 deletion src/lib/actions/sandbox/forward-health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ export type SandboxForwardListEntry = {

export type SandboxForwardHealth = boolean | "occupied" | null;

/** Whether OpenShell reports a forward as live in either supported CLI vocabulary. */
export function isLiveSandboxForwardStatus(status: string): boolean {
return status === "running" || status === "active";
}

function liveEntriesForPort(
entries: SandboxForwardListEntry[],
port: string,
): SandboxForwardListEntry[] {
return entries.filter((entry) => entry.port === port && entry.status === "running");
return entries.filter((entry) => entry.port === port && isLiveSandboxForwardStatus(entry.status));
}

export function classifySandboxForwardHealth(
Expand Down
2 changes: 2 additions & 0 deletions src/lib/actions/sandbox/forward-recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ import {
} from "./hermes-dashboard-recovery";
export {
HermesPortableForwardRecoveryError,
prepareHermesPortableLaunchForwards,
recoverHermesPortableLaunchForwards,
} from "./probe/hermes-portable-forward-recovery";
export type {
HermesPortableForwardRecoveryFailure,
HermesPortableForwardRecoveryInput,
HermesPortableForwardRecoveryResult,
PreparedHermesPortableForwardRecovery,
} from "./probe/hermes-portable-forward-recovery";

type SandboxPortAgent = {
Expand Down
Loading
Loading