Skip to content
Merged
39 changes: 38 additions & 1 deletion src/nemoclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ function getNamedGatewayLifecycleState() {
return { state: "missing_named", status: status.output, gatewayInfo: gatewayInfo.output };
}

/** Attempt to recover the named NemoClaw gateway after a restart or connectivity loss. */
async function recoverNamedGatewayRuntime() {
const before = getNamedGatewayLifecycleState();
if (before.state === "healthy_named") {
Expand Down Expand Up @@ -550,10 +551,45 @@ async function recoverNamedGatewayRuntime() {
return { recovered: false, before, after, attempted: true };
}

/** Query sandbox presence and return its output with the live enforced policy. */
function getSandboxGatewayState(sandboxName) {
const result = captureOpenshell(["sandbox", "get", sandboxName]);
const output = result.output;
let output = result.output;
if (result.status === 0) {
// `openshell sandbox get` returns the immutable baseline policy from sandbox
// creation, which does not include network_policies added later via
// `openshell policy set`. Replace the Policy section with the live policy
// from `policy get --full`, preserving the colored "Policy:" header and
// Sandbox info above it. (#1132)
const livePolicy = captureOpenshell(["policy", "get", "--full", sandboxName], {
ignoreError: true,
});
if (livePolicy.status === 0 && livePolicy.output.trim()) {
const rawLines = String(output).split("\n");
const cleanLines = stripAnsi(String(output)).split("\n");
const policyLineIdx = cleanLines.findIndex((l) => l.trim() === "Policy:");
if (policyLineIdx !== -1) {
// Keep everything before Policy (Sandbox info with colors),
// plus the original colored "Policy:" header line.
const before = rawLines.slice(0, policyLineIdx + 1).join("\n");
// Extract YAML content from policy get --full (skip metadata header before "---").
// Use a regex to handle varying line endings (\n, \r\n) and optional trailing whitespace.
const delimIdx = livePolicy.output.search(/^---\s*$/m);
const yamlPart = delimIdx !== -1
? livePolicy.output.slice(delimIdx).replace(/^---\s*[\r\n]+/, "")
: livePolicy.output;
// Guard: only replace if the extracted content looks like policy YAML
// (starts with a YAML key like "version:" or "network_policies:").
// Avoids replacing with warnings or status text from unexpected output.
const trimmedYaml = yamlPart.trim();
const looksLikeError = /^(error|failed|invalid|warning|status)\b/i.test(trimmedYaml);
if (trimmedYaml && !looksLikeError && /^[a-z_][a-z0-9_]*\s*:/m.test(trimmedYaml)) {
// Add 2-space indent to match the original sandbox get output format.
const indented = trimmedYaml.split("\n").map((l) => (l ? " " + l : l)).join("\n");
output = before + "\n\n" + indented + "\n";
}
}
}
return { state: "present", output };
}
if (/\bNotFound\b|\bNot Found\b|sandbox not found/i.test(output)) {
Expand All @@ -569,6 +605,7 @@ function getSandboxGatewayState(sandboxName) {
return { state: "unknown_error", output };
}

/** Print troubleshooting hints based on gateway lifecycle state in the output. */
function printGatewayLifecycleHint(output = "", sandboxName = "", writer = console.error) {
const cleanOutput = stripAnsi(output);
if (/No gateway configured/i.test(cleanOutput)) {
Expand Down
Loading