Skip to content

Commit 5d6bf0e

Browse files
1PoPTRoNapurvvkumaria
authored andcommitted
fix(hermes): prove broker ownership from a live pid
Refuse a healthy managed-tool listener unless its recorded pid still resolves to the NemoClaw broker. This prevents a stale in-process ownership latch from adopting a foreign process after the broker exits while retaining the registered-process reuse path and operator recovery message. Signed-off-by: 1PoPTRoN <vrxn.arp1traj@gmail.com>
1 parent d7c33bf commit 5d6bf0e

2 files changed

Lines changed: 707 additions & 643 deletions

File tree

src/lib/hermes-tool-gateway-broker.ts

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ const HERMES_TOOL_GATEWAY_CONTROL_CONTRACT_PATH = path.join(
6363
);
6464
const HERMES_TOOL_GATEWAY_RUNTIME_MISMATCH_RECOVERY =
6565
"Reauthorize every managed-tool Hermes sandbox, then retry.";
66+
const HERMES_TOOL_GATEWAY_UNOWNED_LISTENER_RECOVERY =
67+
"Stop the process holding that port, then retry.";
6668
const HERMES_TOOL_GATEWAY_CONTROL_CLIENT_SOURCE = [
6769
'const http = require("node:http");',
6870
"const [socketPath, route, timeoutValue] = process.argv.slice(1);",
@@ -118,8 +120,6 @@ const HERMES_TOOL_GATEWAY_CONTROL_CLIENT_SOURCE = [
118120
"});",
119121
].join("\n");
120122

121-
let brokerStartedThisRun = false;
122-
123123
function sleep(ms) {
124124
const lock = new Int32Array(new SharedArrayBuffer(4));
125125
Atomics.wait(lock, 0, 0, ms);
@@ -555,7 +555,7 @@ function preflightHermesToolGatewayCloneBinding(sandboxName) {
555555
}
556556

557557
const pid = readPid();
558-
const currentBrokerOwned = isHermesToolGatewayBrokerProcess(pid) || brokerStartedThisRun;
558+
const currentBrokerOwned = isHermesToolGatewayBrokerProcess(pid);
559559
const currentBrokerHealthy = isHermesToolGatewayBrokerHealthy();
560560
if (currentBrokerHealthy && !currentBrokerOwned) {
561561
throw new Error("Hermes managed-tool broker health endpoint is not owned by NemoClaw");
@@ -738,8 +738,23 @@ function ensureHermesToolGatewayBroker(options = {}) {
738738
const desiredHash = brokerRuntimeHash();
739739
const hashMatches = readBrokerHash() === desiredHash;
740740
const pid = readPid();
741-
const currentBrokerOwned = isHermesToolGatewayBrokerProcess(pid) || brokerStartedThisRun;
742-
const currentBrokerHealthy = currentBrokerOwned && isHermesToolGatewayBrokerHealthy();
741+
const currentBrokerOwned = isHermesToolGatewayBrokerProcess(pid);
742+
const brokerHealthy = isHermesToolGatewayBrokerHealthy();
743+
const currentBrokerHealthy = currentBrokerOwned && brokerHealthy;
744+
// `/health` is unauthenticated on a fixed port, so reachability proves
745+
// liveness and never identity. Ownership comes only from a recorded pid that
746+
// still resolves to a running broker, re-proved on every call: a broker can
747+
// exit and leave the port free for another process to bind. Refuse before any
748+
// path can adopt, restart around, or stage credentials against a listener
749+
// NemoClaw cannot prove it owns.
750+
if (brokerHealthy && !currentBrokerOwned) {
751+
console.error(
752+
"Hermes managed-tool broker health endpoint is not owned by NemoClaw; " +
753+
`refusing to reuse the listener on port ${HERMES_TOOL_GATEWAY_PORT}. ` +
754+
HERMES_TOOL_GATEWAY_UNOWNED_LISTENER_RECOVERY,
755+
);
756+
return false;
757+
}
743758
if (options.startWithoutCredential) {
744759
if (currentBrokerHealthy) {
745760
return hashMatches && fs.existsSync(HERMES_TOOL_GATEWAY_CONTROL_SOCKET_PATH);
@@ -752,7 +767,6 @@ function ensureHermesToolGatewayBroker(options = {}) {
752767
isHermesToolGatewayBrokerHealthy() &&
753768
fs.existsSync(HERMES_TOOL_GATEWAY_CONTROL_SOCKET_PATH)
754769
) {
755-
brokerStartedThisRun = true;
756770
return true;
757771
}
758772
sleep(250);
@@ -779,7 +793,6 @@ function ensureHermesToolGatewayBroker(options = {}) {
779793
refreshToken,
780794
options.sandboxName ?? null,
781795
);
782-
if (registered) brokerStartedThisRun = true;
783796
return registered;
784797
}
785798
if (refreshPlan === "start-or-restart") {
@@ -791,33 +804,16 @@ function ensureHermesToolGatewayBroker(options = {}) {
791804
isHermesToolGatewayBrokerHealthy() &&
792805
registerHermesToolGatewayRuntimeCredential(refreshToken, options.sandboxName ?? null)
793806
) {
794-
brokerStartedThisRun = true;
795807
return true;
796808
}
797809
sleep(250);
798810
}
799811
return false;
800812
}
801813

802-
if (
803-
!options.forceRestart &&
804-
hashMatches &&
805-
brokerStartedThisRun &&
806-
isHermesToolGatewayBrokerHealthy()
807-
) {
808-
return true;
809-
}
810-
if (
811-
!options.forceRestart &&
812-
hashMatches &&
813-
isHermesToolGatewayBrokerProcess(pid) &&
814-
isHermesToolGatewayBrokerHealthy()
815-
) {
816-
brokerStartedThisRun = true;
817-
return true;
818-
}
819-
if (!options.forceRestart && hashMatches && isHermesToolGatewayBrokerHealthy()) {
820-
brokerStartedThisRun = true;
814+
// `currentBrokerHealthy` already requires ownership, covering both proofs the
815+
// three former branches tested separately, so reuse is one condition.
816+
if (!options.forceRestart && hashMatches && currentBrokerHealthy) {
821817
return true;
822818
}
823819
// Raw Nous OAuth stays out of durable ~/.nemoclaw state. If the broker is

0 commit comments

Comments
 (0)