Skip to content
49 changes: 49 additions & 0 deletions bin/lib/policies.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const fs = require("fs");
const path = require("path");
const os = require("os");
const readline = require("readline");
const YAML = require("yaml");
const { ROOT, run, runCapture, shellQuote } = require("./runner");
const registry = require("./registry");
Expand Down Expand Up @@ -288,6 +289,53 @@ function getAppliedPresets(sandboxName) {
return sandbox ? sandbox.policies || [] : [];
}

function selectFromList(items, { applied = [] } = {}) {
return new Promise((resolve) => {
process.stderr.write("\n Available presets:\n");
items.forEach((item, i) => {
const marker = applied.includes(item.name) ? "●" : "○";
const description = item.description ? ` — ${item.description}` : "";
process.stderr.write(` ${i + 1}) ${marker} ${item.name}${description}\n`);
});
process.stderr.write("\n ● applied, ○ not applied\n\n");
const defaultIdx = items.findIndex((item) => !applied.includes(item.name));
const defaultNum = defaultIdx >= 0 ? defaultIdx + 1 : null;
const question = defaultNum ? ` Choose preset [${defaultNum}]: ` : " Choose preset: ";
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
rl.question(question, (answer) => {
rl.close();
if (!process.stdin.isTTY) {
if (typeof process.stdin.pause === "function") process.stdin.pause();
if (typeof process.stdin.unref === "function") process.stdin.unref();
}
const trimmed = answer.trim();
const effectiveInput = trimmed || (defaultNum ? String(defaultNum) : "");
if (!effectiveInput) {
resolve(null);
return;
}
if (!/^\d+$/.test(effectiveInput)) {
process.stderr.write("\n Invalid preset number.\n");
resolve(null);
return;
}
const num = Number(effectiveInput);
const item = items[num - 1];
if (!item) {
process.stderr.write("\n Invalid preset number.\n");
resolve(null);
return;
}
if (applied.includes(item.name)) {
process.stderr.write(`\n Preset '${item.name}' is already applied.\n`);
resolve(null);
return;
}
resolve(item.name);
});
});
}

module.exports = {
PRESETS_DIR,
listPresets,
Expand All @@ -300,4 +348,5 @@ module.exports = {
mergePresetIntoPolicy,
applyPreset,
getAppliedPresets,
selectFromList,
};
77 changes: 77 additions & 0 deletions bin/lib/telegram-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/**
* Telegram Bot API client with socket timeout protection.
*
* Exported so both the bridge script and tests use the same implementation.
*/

const https = require("https");

const DEFAULT_TIMEOUT_MS = 60000;

/**
* Call a Telegram Bot API method.
*
* @param {string} token - Bot token from BotFather
* @param {string} method - API method name (e.g. "getUpdates")
* @param {object} body - JSON-serialisable request body
* @param {object} [opts]
* @param {number} [opts.timeout] - socket idle timeout in ms (default 60 000)
* @param {string} [opts.hostname] - override hostname (useful for tests)
* @param {number} [opts.port] - override port (useful for tests)
* @param {boolean} [opts.rejectUnauthorized] - TLS cert check (default true)
* @returns {Promise<object>} parsed JSON response
*/
function tgApi(token, method, body, opts = {}) {
const {
timeout = DEFAULT_TIMEOUT_MS,
hostname = "api.telegram.org",
port,
rejectUnauthorized,
} = opts;

return new Promise((resolve, reject) => {
let settled = false;
const settle = (fn, value) => {
if (settled) return;
settled = true;
fn(value);
};

const data = JSON.stringify(body);
const reqOpts = {
hostname,
path: `/bot${token}/${method}`,
method: "POST",
timeout,
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) },
};
if (port != null) reqOpts.port = port;
if (rejectUnauthorized != null) reqOpts.rejectUnauthorized = rejectUnauthorized;

const req = https.request(reqOpts, (res) => {
let buf = "";
res.setEncoding("utf8");
res.on("data", (c) => (buf += c));
res.on("aborted", () => settle(reject, new Error(`Telegram API ${method} response aborted`)));
res.on("error", (err) => settle(reject, err));
res.on("end", () => {
try {
settle(resolve, JSON.parse(buf));
} catch {
settle(resolve, { ok: false, error: buf });
}
});
});
req.on("timeout", () => {
req.destroy(new Error(`Telegram API ${method} timed out`));
});
req.on("error", (err) => settle(reject, err));
req.write(data);
req.end();
});
}

module.exports = { tgApi, DEFAULT_TIMEOUT_MS };
10 changes: 1 addition & 9 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -1109,16 +1109,8 @@ async function sandboxPolicyAdd(sandboxName) {
const allPresets = policies.listPresets();
const applied = policies.getAppliedPresets(sandboxName);

console.log("");
console.log(" Available presets:");
allPresets.forEach((p) => {
const marker = applied.includes(p.name) ? "●" : "○";
console.log(` ${marker} ${p.name} — ${p.description}`);
});
console.log("");

const { prompt: askPrompt } = require("./lib/credentials");
const answer = await askPrompt(" Preset to apply: ");
const answer = await policies.selectFromList(allPresets, { applied });
if (!answer) return;

const confirm = await askPrompt(` Apply '${answer}' to sandbox '${sandboxName}'? [Y/n]: `);
Expand Down
Loading