Sync dsh-netxops 0.1.41 with NetX async exec jobs and capability hints.

Add netx__getNeExecJob, route execManagedNe async to /exec-jobs, and document getManagedNe.capability for agents.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-06 18:32:17 +08:00
parent 4dd628b7cc
commit 30d4df23fa
14 changed files with 740 additions and 58 deletions

View file

@ -18,7 +18,7 @@ Model names: `netx__<stem>`. NMS tools use `Nms`; adapter `nmsProvider=zte-ume`
| Tool | Role |
|------|------|
| `queryNmsAlarms` … `sqlQueryNms` | Alarm + inventory + SQL |
| `listManagedNe` / `getManagedNe` / `execManagedNe` / `listCliTargets` | Managed CLI (login / show) |
| `listManagedNe` / `getManagedNe` / `execManagedNe` / `getNeExecJob` / `listCliTargets` | Managed CLI (login / show / async jobs) |
| `findTopologyPaths` | Fabric path lookup |
## bizMonitor → `netx-biz-monitor`

View file

@ -18,6 +18,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
"netx__getNeExecJob",
"netx__listCliTargets",
"netx__findTopologyPaths"
]),
@ -1133,7 +1134,60 @@ function quoteNeId(neId) {
}
// src/netx/handlers.ts
var EXEC_MAX_COMMANDS = 5;
var EXEC_MAX_COMMANDS_CAP = 50;
var EXEC_MAX_COMMANDS_DEFAULT = 5;
function execMaxCommands() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_MAX_COMMANDS ?? ""), 10);
const n = Number.isFinite(raw) ? raw : EXEC_MAX_COMMANDS_DEFAULT;
return Math.max(1, Math.min(EXEC_MAX_COMMANDS_CAP, n));
}
function asyncMinNes() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_ASYNC_MIN_NES ?? ""), 10);
const n = Number.isFinite(raw) ? raw : 4;
return Math.max(0, Math.min(50, n));
}
function truthyAsyncFlag(raw) {
if (raw === undefined || raw === null)
return;
if (typeof raw === "boolean")
return raw;
const text = String(raw).trim().toLowerCase();
if (!text)
return;
if (["1", "true", "yes", "on"].includes(text))
return true;
if (["0", "false", "no", "off"].includes(text))
return false;
return;
}
function countExecNeTargets(args) {
let n = 0;
for (const key of ["ne_ids", "nms_ne_ids", "ume_ne_ids"]) {
const val = strList(args, key);
if (val.length > 0)
n = Math.max(n, val.length);
}
const targets = args.targets;
if (Array.isArray(targets)) {
n = Math.max(n, targets.filter((t) => typeof t === "object" && t !== null && !Array.isArray(t)).length);
}
if (n === 0) {
if (str2(args, "ne_id").trim() || nmsOrUme(args, "nms_ne_id", "ume_ne_id"))
return 1;
}
return n;
}
function shouldRunExecAsync(args) {
const flag = truthyAsyncFlag(args.async);
if (flag === false)
return false;
if (flag === true)
return true;
const minN = asyncMinNes();
if (minN <= 0)
return false;
return countExecNeTargets(args) >= minN;
}
var UME_RAW_FIELD_PRESETS = {
brief: [
"alarm_alarm_key",
@ -1375,6 +1429,8 @@ async function getManagedNe(client, args, signal) {
return out;
}
async function execManagedNe(client, args, signal) {
const maxCmds = execMaxCommands();
const wantAsync = shouldRunExecAsync(args);
const targetsRaw = args.targets;
const neIds = strList(args, "ne_ids");
const umeNeIds = nmsOrUmeList(args, "nms_ne_ids", "ume_ne_ids");
@ -1408,7 +1464,7 @@ async function execManagedNe(client, args, signal) {
if (umeNeIds.length > 0)
body2.ume_ne_ids = umeNeIds;
if (sharedCommands.length > 0) {
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
body2.commands = sharedCommands;
@ -1417,6 +1473,13 @@ async function execManagedNe(client, args, signal) {
const concurrency = num2(args, "concurrency");
if (concurrency !== undefined)
body2.concurrency = clampInt(concurrency, 4, 1, 8);
if (wantAsync) {
const out3 = await client.post("/v1/managed-ne/exec-jobs", body2, signal, 60000);
if (out3.ok !== true)
return out3;
const data3 = asRecord(out3.data);
return Object.keys(data3).length > 0 ? data3 : out3;
}
const out2 = await client.post("/v1/managed-ne/exec-batch", body2, signal, 600000);
if (out2.ok !== true)
return out2;
@ -1439,7 +1502,7 @@ async function execManagedNe(client, args, signal) {
if (sharedCommands.length === 0) {
return { ok: false, error: "commands_required", error_code: "commands_required" };
}
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
const body = {
@ -1450,6 +1513,13 @@ async function execManagedNe(client, args, signal) {
body.ne_id = neId;
if (umeNeId)
body.ume_ne_id = umeNeId;
if (wantAsync) {
const out2 = await client.post("/v1/managed-ne/exec-jobs", body, signal, 60000);
if (out2.ok !== true)
return out2;
const data2 = asRecord(out2.data);
return Object.keys(data2).length > 0 ? data2 : out2;
}
const out = await client.post("/v1/managed-ne/exec", body, signal, 300000);
if (out.ok !== true)
return out;
@ -1459,6 +1529,18 @@ async function execManagedNe(client, args, signal) {
}
return { ok: true, data };
}
async function getNeExecJob(client, args, signal) {
const jobId = str2(args, "job_id").trim();
if (!jobId) {
return {
ok: false,
error: "job_id_required",
error_code: "job_id_required",
hint: "Pass job_id from execManagedNe async ack."
};
}
return client.get(`/v1/managed-ne/exec-jobs/${quoteNeId(jobId)}`, undefined, signal);
}
async function listCliTargets(client, args, signal) {
const params = {
page: clampInt(num2(args, "page"), 1, 1, Number.MAX_SAFE_INTEGER),
@ -2614,12 +2696,12 @@ function registerNetxTools(ctx, connection, options) {
page: num4(),
page_size: num4()
}, listManagedNe, getClient, t),
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Do NOT pass NMS inventory UUID here.", {
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Response includes capability (device_family, exec_policy_effective, recommended_mode, hints) — read before complex execManagedNe. Do NOT pass NMS inventory UUID here.", {
ne_id: str4("Managed NE id"),
managed_ne_id: str4("Alias for ne_id"),
id: str4("Alias for ne_id")
}, getManagedNe, getClient, t),
tool2("netx__execManagedNe", "Run read-only CLI via netx (show/display/ping/traceroute). Single NE: ne_id OR ume_ne_id + commands. Many NEs: ne_ids[]/ume_ne_ids[] + shared commands, or targets[{ume_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work.", {
tool2("netx__execManagedNe", "Run CLI via netx (readonly show/display/ping/traceroute; linux_shell on Linux/MikroTik allows scripts). Single NE: ne_id OR nms_ne_id + commands. Many NEs: ne_ids[]/nms_ne_ids[] + shared commands, or targets[{nms_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work. Long or multi-NE: async=true (or auto when ≥4 NEs) returns job_id — poll getNeExecJob. Read getManagedNe.capability first.", {
ne_id: str4(),
nms_ne_id: str4("NMS inventory id; alias ume_ne_id"),
ume_ne_id: str4("Legacy alias of nms_ne_id"),
@ -2628,7 +2710,7 @@ function registerNetxTools(ctx, connection, options) {
ume_ne_ids: strArr("Legacy alias of nms_ne_ids"),
targets: {
type: "array",
description: "Per-NE command sets: each item is one NE (ne_id OR ume_ne_id) with commands[].",
description: "Per-NE command sets: each item is one NE (ne_id OR nms_ne_id) with commands[].",
items: {
type: "object",
additionalProperties: false,
@ -2643,8 +2725,11 @@ function registerNetxTools(ctx, connection, options) {
commands: strArr("Commands for single NE, or shared commands for batch."),
read_timeout_sec: num4("Per-command read timeout (default 60; use 90–120 for slow show)."),
concurrency: num4("Parallel NEs for batch mode (1–8, default 4)."),
async: bool4("oclaw-only async hint; ignored by native REST client.")
async: bool4("true=background job_id + getNeExecJob; false=force sync; omit=auto for ~4+ NEs (NETX_NE_EXEC_ASYNC_MIN_NES).")
}, execManagedNe, getClient, Math.max(t, 300000)),
tool2("netx__getNeExecJob", "Poll a background execManagedNe job (job_id from async ack). When terminal=true, result holds exec/exec-batch payload. Do not busy-wait in the same turn.", {
job_id: str4("Job id from execManagedNe async response.")
}, getNeExecJob, getClient, Math.max(t, 60000)),
tool2("netx__listCliTargets", "List CLI-capable targets (managed NE and/or NMS inventory). Call once per session with keyword/source, cache ids, then execManagedNe.", {
source: str4("managed | nms | ume | all"),
keyword: str4(),

View file

@ -18,6 +18,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
"netx__getNeExecJob",
"netx__listCliTargets",
"netx__findTopologyPaths"
]),
@ -1133,7 +1134,60 @@ function quoteNeId(neId) {
}
// src/netx/handlers.ts
var EXEC_MAX_COMMANDS = 5;
var EXEC_MAX_COMMANDS_CAP = 50;
var EXEC_MAX_COMMANDS_DEFAULT = 5;
function execMaxCommands() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_MAX_COMMANDS ?? ""), 10);
const n = Number.isFinite(raw) ? raw : EXEC_MAX_COMMANDS_DEFAULT;
return Math.max(1, Math.min(EXEC_MAX_COMMANDS_CAP, n));
}
function asyncMinNes() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_ASYNC_MIN_NES ?? ""), 10);
const n = Number.isFinite(raw) ? raw : 4;
return Math.max(0, Math.min(50, n));
}
function truthyAsyncFlag(raw) {
if (raw === undefined || raw === null)
return;
if (typeof raw === "boolean")
return raw;
const text = String(raw).trim().toLowerCase();
if (!text)
return;
if (["1", "true", "yes", "on"].includes(text))
return true;
if (["0", "false", "no", "off"].includes(text))
return false;
return;
}
function countExecNeTargets(args) {
let n = 0;
for (const key of ["ne_ids", "nms_ne_ids", "ume_ne_ids"]) {
const val = strList(args, key);
if (val.length > 0)
n = Math.max(n, val.length);
}
const targets = args.targets;
if (Array.isArray(targets)) {
n = Math.max(n, targets.filter((t) => typeof t === "object" && t !== null && !Array.isArray(t)).length);
}
if (n === 0) {
if (str2(args, "ne_id").trim() || nmsOrUme(args, "nms_ne_id", "ume_ne_id"))
return 1;
}
return n;
}
function shouldRunExecAsync(args) {
const flag = truthyAsyncFlag(args.async);
if (flag === false)
return false;
if (flag === true)
return true;
const minN = asyncMinNes();
if (minN <= 0)
return false;
return countExecNeTargets(args) >= minN;
}
var UME_RAW_FIELD_PRESETS = {
brief: [
"alarm_alarm_key",
@ -1375,6 +1429,8 @@ async function getManagedNe(client, args, signal) {
return out;
}
async function execManagedNe(client, args, signal) {
const maxCmds = execMaxCommands();
const wantAsync = shouldRunExecAsync(args);
const targetsRaw = args.targets;
const neIds = strList(args, "ne_ids");
const umeNeIds = nmsOrUmeList(args, "nms_ne_ids", "ume_ne_ids");
@ -1408,7 +1464,7 @@ async function execManagedNe(client, args, signal) {
if (umeNeIds.length > 0)
body2.ume_ne_ids = umeNeIds;
if (sharedCommands.length > 0) {
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
body2.commands = sharedCommands;
@ -1417,6 +1473,13 @@ async function execManagedNe(client, args, signal) {
const concurrency = num2(args, "concurrency");
if (concurrency !== undefined)
body2.concurrency = clampInt(concurrency, 4, 1, 8);
if (wantAsync) {
const out3 = await client.post("/v1/managed-ne/exec-jobs", body2, signal, 60000);
if (out3.ok !== true)
return out3;
const data3 = asRecord(out3.data);
return Object.keys(data3).length > 0 ? data3 : out3;
}
const out2 = await client.post("/v1/managed-ne/exec-batch", body2, signal, 600000);
if (out2.ok !== true)
return out2;
@ -1439,7 +1502,7 @@ async function execManagedNe(client, args, signal) {
if (sharedCommands.length === 0) {
return { ok: false, error: "commands_required", error_code: "commands_required" };
}
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
const body = {
@ -1450,6 +1513,13 @@ async function execManagedNe(client, args, signal) {
body.ne_id = neId;
if (umeNeId)
body.ume_ne_id = umeNeId;
if (wantAsync) {
const out2 = await client.post("/v1/managed-ne/exec-jobs", body, signal, 60000);
if (out2.ok !== true)
return out2;
const data2 = asRecord(out2.data);
return Object.keys(data2).length > 0 ? data2 : out2;
}
const out = await client.post("/v1/managed-ne/exec", body, signal, 300000);
if (out.ok !== true)
return out;
@ -1459,6 +1529,18 @@ async function execManagedNe(client, args, signal) {
}
return { ok: true, data };
}
async function getNeExecJob(client, args, signal) {
const jobId = str2(args, "job_id").trim();
if (!jobId) {
return {
ok: false,
error: "job_id_required",
error_code: "job_id_required",
hint: "Pass job_id from execManagedNe async ack."
};
}
return client.get(`/v1/managed-ne/exec-jobs/${quoteNeId(jobId)}`, undefined, signal);
}
async function listCliTargets(client, args, signal) {
const params = {
page: clampInt(num2(args, "page"), 1, 1, Number.MAX_SAFE_INTEGER),
@ -2614,12 +2696,12 @@ function registerNetxTools(ctx, connection, options) {
page: num4(),
page_size: num4()
}, listManagedNe, getClient, t),
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Do NOT pass NMS inventory UUID here.", {
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Response includes capability (device_family, exec_policy_effective, recommended_mode, hints) — read before complex execManagedNe. Do NOT pass NMS inventory UUID here.", {
ne_id: str4("Managed NE id"),
managed_ne_id: str4("Alias for ne_id"),
id: str4("Alias for ne_id")
}, getManagedNe, getClient, t),
tool2("netx__execManagedNe", "Run read-only CLI via netx (show/display/ping/traceroute). Single NE: ne_id OR ume_ne_id + commands. Many NEs: ne_ids[]/ume_ne_ids[] + shared commands, or targets[{ume_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work.", {
tool2("netx__execManagedNe", "Run CLI via netx (readonly show/display/ping/traceroute; linux_shell on Linux/MikroTik allows scripts). Single NE: ne_id OR nms_ne_id + commands. Many NEs: ne_ids[]/nms_ne_ids[] + shared commands, or targets[{nms_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work. Long or multi-NE: async=true (or auto when ≥4 NEs) returns job_id — poll getNeExecJob. Read getManagedNe.capability first.", {
ne_id: str4(),
nms_ne_id: str4("NMS inventory id; alias ume_ne_id"),
ume_ne_id: str4("Legacy alias of nms_ne_id"),
@ -2628,7 +2710,7 @@ function registerNetxTools(ctx, connection, options) {
ume_ne_ids: strArr("Legacy alias of nms_ne_ids"),
targets: {
type: "array",
description: "Per-NE command sets: each item is one NE (ne_id OR ume_ne_id) with commands[].",
description: "Per-NE command sets: each item is one NE (ne_id OR nms_ne_id) with commands[].",
items: {
type: "object",
additionalProperties: false,
@ -2643,8 +2725,11 @@ function registerNetxTools(ctx, connection, options) {
commands: strArr("Commands for single NE, or shared commands for batch."),
read_timeout_sec: num4("Per-command read timeout (default 60; use 90–120 for slow show)."),
concurrency: num4("Parallel NEs for batch mode (1–8, default 4)."),
async: bool4("oclaw-only async hint; ignored by native REST client.")
async: bool4("true=background job_id + getNeExecJob; false=force sync; omit=auto for ~4+ NEs (NETX_NE_EXEC_ASYNC_MIN_NES).")
}, execManagedNe, getClient, Math.max(t, 300000)),
tool2("netx__getNeExecJob", "Poll a background execManagedNe job (job_id from async ack). When terminal=true, result holds exec/exec-batch payload. Do not busy-wait in the same turn.", {
job_id: str4("Job id from execManagedNe async response.")
}, getNeExecJob, getClient, Math.max(t, 60000)),
tool2("netx__listCliTargets", "List CLI-capable targets (managed NE and/or NMS inventory). Call once per session with keyword/source, cache ids, then execManagedNe.", {
source: str4("managed | nms | ume | all"),
keyword: str4(),

View file

@ -18,6 +18,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
"netx__getNeExecJob",
"netx__listCliTargets",
"netx__findTopologyPaths"
]),
@ -1133,7 +1134,60 @@ function quoteNeId(neId) {
}
// src/netx/handlers.ts
var EXEC_MAX_COMMANDS = 5;
var EXEC_MAX_COMMANDS_CAP = 50;
var EXEC_MAX_COMMANDS_DEFAULT = 5;
function execMaxCommands() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_MAX_COMMANDS ?? ""), 10);
const n = Number.isFinite(raw) ? raw : EXEC_MAX_COMMANDS_DEFAULT;
return Math.max(1, Math.min(EXEC_MAX_COMMANDS_CAP, n));
}
function asyncMinNes() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_ASYNC_MIN_NES ?? ""), 10);
const n = Number.isFinite(raw) ? raw : 4;
return Math.max(0, Math.min(50, n));
}
function truthyAsyncFlag(raw) {
if (raw === undefined || raw === null)
return;
if (typeof raw === "boolean")
return raw;
const text = String(raw).trim().toLowerCase();
if (!text)
return;
if (["1", "true", "yes", "on"].includes(text))
return true;
if (["0", "false", "no", "off"].includes(text))
return false;
return;
}
function countExecNeTargets(args) {
let n = 0;
for (const key of ["ne_ids", "nms_ne_ids", "ume_ne_ids"]) {
const val = strList(args, key);
if (val.length > 0)
n = Math.max(n, val.length);
}
const targets = args.targets;
if (Array.isArray(targets)) {
n = Math.max(n, targets.filter((t) => typeof t === "object" && t !== null && !Array.isArray(t)).length);
}
if (n === 0) {
if (str2(args, "ne_id").trim() || nmsOrUme(args, "nms_ne_id", "ume_ne_id"))
return 1;
}
return n;
}
function shouldRunExecAsync(args) {
const flag = truthyAsyncFlag(args.async);
if (flag === false)
return false;
if (flag === true)
return true;
const minN = asyncMinNes();
if (minN <= 0)
return false;
return countExecNeTargets(args) >= minN;
}
var UME_RAW_FIELD_PRESETS = {
brief: [
"alarm_alarm_key",
@ -1375,6 +1429,8 @@ async function getManagedNe(client, args, signal) {
return out;
}
async function execManagedNe(client, args, signal) {
const maxCmds = execMaxCommands();
const wantAsync = shouldRunExecAsync(args);
const targetsRaw = args.targets;
const neIds = strList(args, "ne_ids");
const umeNeIds = nmsOrUmeList(args, "nms_ne_ids", "ume_ne_ids");
@ -1408,7 +1464,7 @@ async function execManagedNe(client, args, signal) {
if (umeNeIds.length > 0)
body2.ume_ne_ids = umeNeIds;
if (sharedCommands.length > 0) {
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
body2.commands = sharedCommands;
@ -1417,6 +1473,13 @@ async function execManagedNe(client, args, signal) {
const concurrency = num2(args, "concurrency");
if (concurrency !== undefined)
body2.concurrency = clampInt(concurrency, 4, 1, 8);
if (wantAsync) {
const out3 = await client.post("/v1/managed-ne/exec-jobs", body2, signal, 60000);
if (out3.ok !== true)
return out3;
const data3 = asRecord(out3.data);
return Object.keys(data3).length > 0 ? data3 : out3;
}
const out2 = await client.post("/v1/managed-ne/exec-batch", body2, signal, 600000);
if (out2.ok !== true)
return out2;
@ -1439,7 +1502,7 @@ async function execManagedNe(client, args, signal) {
if (sharedCommands.length === 0) {
return { ok: false, error: "commands_required", error_code: "commands_required" };
}
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
const body = {
@ -1450,6 +1513,13 @@ async function execManagedNe(client, args, signal) {
body.ne_id = neId;
if (umeNeId)
body.ume_ne_id = umeNeId;
if (wantAsync) {
const out2 = await client.post("/v1/managed-ne/exec-jobs", body, signal, 60000);
if (out2.ok !== true)
return out2;
const data2 = asRecord(out2.data);
return Object.keys(data2).length > 0 ? data2 : out2;
}
const out = await client.post("/v1/managed-ne/exec", body, signal, 300000);
if (out.ok !== true)
return out;
@ -1459,6 +1529,18 @@ async function execManagedNe(client, args, signal) {
}
return { ok: true, data };
}
async function getNeExecJob(client, args, signal) {
const jobId = str2(args, "job_id").trim();
if (!jobId) {
return {
ok: false,
error: "job_id_required",
error_code: "job_id_required",
hint: "Pass job_id from execManagedNe async ack."
};
}
return client.get(`/v1/managed-ne/exec-jobs/${quoteNeId(jobId)}`, undefined, signal);
}
async function listCliTargets(client, args, signal) {
const params = {
page: clampInt(num2(args, "page"), 1, 1, Number.MAX_SAFE_INTEGER),
@ -2614,12 +2696,12 @@ function registerNetxTools(ctx, connection, options) {
page: num4(),
page_size: num4()
}, listManagedNe, getClient, t),
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Do NOT pass NMS inventory UUID here.", {
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Response includes capability (device_family, exec_policy_effective, recommended_mode, hints) — read before complex execManagedNe. Do NOT pass NMS inventory UUID here.", {
ne_id: str4("Managed NE id"),
managed_ne_id: str4("Alias for ne_id"),
id: str4("Alias for ne_id")
}, getManagedNe, getClient, t),
tool2("netx__execManagedNe", "Run read-only CLI via netx (show/display/ping/traceroute). Single NE: ne_id OR ume_ne_id + commands. Many NEs: ne_ids[]/ume_ne_ids[] + shared commands, or targets[{ume_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work.", {
tool2("netx__execManagedNe", "Run CLI via netx (readonly show/display/ping/traceroute; linux_shell on Linux/MikroTik allows scripts). Single NE: ne_id OR nms_ne_id + commands. Many NEs: ne_ids[]/nms_ne_ids[] + shared commands, or targets[{nms_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work. Long or multi-NE: async=true (or auto when ≥4 NEs) returns job_id — poll getNeExecJob. Read getManagedNe.capability first.", {
ne_id: str4(),
nms_ne_id: str4("NMS inventory id; alias ume_ne_id"),
ume_ne_id: str4("Legacy alias of nms_ne_id"),
@ -2628,7 +2710,7 @@ function registerNetxTools(ctx, connection, options) {
ume_ne_ids: strArr("Legacy alias of nms_ne_ids"),
targets: {
type: "array",
description: "Per-NE command sets: each item is one NE (ne_id OR ume_ne_id) with commands[].",
description: "Per-NE command sets: each item is one NE (ne_id OR nms_ne_id) with commands[].",
items: {
type: "object",
additionalProperties: false,
@ -2643,8 +2725,11 @@ function registerNetxTools(ctx, connection, options) {
commands: strArr("Commands for single NE, or shared commands for batch."),
read_timeout_sec: num4("Per-command read timeout (default 60; use 90–120 for slow show)."),
concurrency: num4("Parallel NEs for batch mode (1–8, default 4)."),
async: bool4("oclaw-only async hint; ignored by native REST client.")
async: bool4("true=background job_id + getNeExecJob; false=force sync; omit=auto for ~4+ NEs (NETX_NE_EXEC_ASYNC_MIN_NES).")
}, execManagedNe, getClient, Math.max(t, 300000)),
tool2("netx__getNeExecJob", "Poll a background execManagedNe job (job_id from async ack). When terminal=true, result holds exec/exec-batch payload. Do not busy-wait in the same turn.", {
job_id: str4("Job id from execManagedNe async response.")
}, getNeExecJob, getClient, Math.max(t, 60000)),
tool2("netx__listCliTargets", "List CLI-capable targets (managed NE and/or NMS inventory). Call once per session with keyword/source, cache ids, then execManagedNe.", {
source: str4("managed | nms | ume | all"),
keyword: str4(),

View file

@ -18,6 +18,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
"netx__getNeExecJob",
"netx__listCliTargets",
"netx__findTopologyPaths"
]),
@ -1133,7 +1134,60 @@ function quoteNeId(neId) {
}
// src/netx/handlers.ts
var EXEC_MAX_COMMANDS = 5;
var EXEC_MAX_COMMANDS_CAP = 50;
var EXEC_MAX_COMMANDS_DEFAULT = 5;
function execMaxCommands() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_MAX_COMMANDS ?? ""), 10);
const n = Number.isFinite(raw) ? raw : EXEC_MAX_COMMANDS_DEFAULT;
return Math.max(1, Math.min(EXEC_MAX_COMMANDS_CAP, n));
}
function asyncMinNes() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_ASYNC_MIN_NES ?? ""), 10);
const n = Number.isFinite(raw) ? raw : 4;
return Math.max(0, Math.min(50, n));
}
function truthyAsyncFlag(raw) {
if (raw === undefined || raw === null)
return;
if (typeof raw === "boolean")
return raw;
const text = String(raw).trim().toLowerCase();
if (!text)
return;
if (["1", "true", "yes", "on"].includes(text))
return true;
if (["0", "false", "no", "off"].includes(text))
return false;
return;
}
function countExecNeTargets(args) {
let n = 0;
for (const key of ["ne_ids", "nms_ne_ids", "ume_ne_ids"]) {
const val = strList(args, key);
if (val.length > 0)
n = Math.max(n, val.length);
}
const targets = args.targets;
if (Array.isArray(targets)) {
n = Math.max(n, targets.filter((t) => typeof t === "object" && t !== null && !Array.isArray(t)).length);
}
if (n === 0) {
if (str2(args, "ne_id").trim() || nmsOrUme(args, "nms_ne_id", "ume_ne_id"))
return 1;
}
return n;
}
function shouldRunExecAsync(args) {
const flag = truthyAsyncFlag(args.async);
if (flag === false)
return false;
if (flag === true)
return true;
const minN = asyncMinNes();
if (minN <= 0)
return false;
return countExecNeTargets(args) >= minN;
}
var UME_RAW_FIELD_PRESETS = {
brief: [
"alarm_alarm_key",
@ -1375,6 +1429,8 @@ async function getManagedNe(client, args, signal) {
return out;
}
async function execManagedNe(client, args, signal) {
const maxCmds = execMaxCommands();
const wantAsync = shouldRunExecAsync(args);
const targetsRaw = args.targets;
const neIds = strList(args, "ne_ids");
const umeNeIds = nmsOrUmeList(args, "nms_ne_ids", "ume_ne_ids");
@ -1408,7 +1464,7 @@ async function execManagedNe(client, args, signal) {
if (umeNeIds.length > 0)
body2.ume_ne_ids = umeNeIds;
if (sharedCommands.length > 0) {
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
body2.commands = sharedCommands;
@ -1417,6 +1473,13 @@ async function execManagedNe(client, args, signal) {
const concurrency = num2(args, "concurrency");
if (concurrency !== undefined)
body2.concurrency = clampInt(concurrency, 4, 1, 8);
if (wantAsync) {
const out3 = await client.post("/v1/managed-ne/exec-jobs", body2, signal, 60000);
if (out3.ok !== true)
return out3;
const data3 = asRecord(out3.data);
return Object.keys(data3).length > 0 ? data3 : out3;
}
const out2 = await client.post("/v1/managed-ne/exec-batch", body2, signal, 600000);
if (out2.ok !== true)
return out2;
@ -1439,7 +1502,7 @@ async function execManagedNe(client, args, signal) {
if (sharedCommands.length === 0) {
return { ok: false, error: "commands_required", error_code: "commands_required" };
}
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
const body = {
@ -1450,6 +1513,13 @@ async function execManagedNe(client, args, signal) {
body.ne_id = neId;
if (umeNeId)
body.ume_ne_id = umeNeId;
if (wantAsync) {
const out2 = await client.post("/v1/managed-ne/exec-jobs", body, signal, 60000);
if (out2.ok !== true)
return out2;
const data2 = asRecord(out2.data);
return Object.keys(data2).length > 0 ? data2 : out2;
}
const out = await client.post("/v1/managed-ne/exec", body, signal, 300000);
if (out.ok !== true)
return out;
@ -1459,6 +1529,18 @@ async function execManagedNe(client, args, signal) {
}
return { ok: true, data };
}
async function getNeExecJob(client, args, signal) {
const jobId = str2(args, "job_id").trim();
if (!jobId) {
return {
ok: false,
error: "job_id_required",
error_code: "job_id_required",
hint: "Pass job_id from execManagedNe async ack."
};
}
return client.get(`/v1/managed-ne/exec-jobs/${quoteNeId(jobId)}`, undefined, signal);
}
async function listCliTargets(client, args, signal) {
const params = {
page: clampInt(num2(args, "page"), 1, 1, Number.MAX_SAFE_INTEGER),
@ -2614,12 +2696,12 @@ function registerNetxTools(ctx, connection, options) {
page: num4(),
page_size: num4()
}, listManagedNe, getClient, t),
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Do NOT pass NMS inventory UUID here.", {
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Response includes capability (device_family, exec_policy_effective, recommended_mode, hints) — read before complex execManagedNe. Do NOT pass NMS inventory UUID here.", {
ne_id: str4("Managed NE id"),
managed_ne_id: str4("Alias for ne_id"),
id: str4("Alias for ne_id")
}, getManagedNe, getClient, t),
tool2("netx__execManagedNe", "Run read-only CLI via netx (show/display/ping/traceroute). Single NE: ne_id OR ume_ne_id + commands. Many NEs: ne_ids[]/ume_ne_ids[] + shared commands, or targets[{ume_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work.", {
tool2("netx__execManagedNe", "Run CLI via netx (readonly show/display/ping/traceroute; linux_shell on Linux/MikroTik allows scripts). Single NE: ne_id OR nms_ne_id + commands. Many NEs: ne_ids[]/nms_ne_ids[] + shared commands, or targets[{nms_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work. Long or multi-NE: async=true (or auto when ≥4 NEs) returns job_id — poll getNeExecJob. Read getManagedNe.capability first.", {
ne_id: str4(),
nms_ne_id: str4("NMS inventory id; alias ume_ne_id"),
ume_ne_id: str4("Legacy alias of nms_ne_id"),
@ -2628,7 +2710,7 @@ function registerNetxTools(ctx, connection, options) {
ume_ne_ids: strArr("Legacy alias of nms_ne_ids"),
targets: {
type: "array",
description: "Per-NE command sets: each item is one NE (ne_id OR ume_ne_id) with commands[].",
description: "Per-NE command sets: each item is one NE (ne_id OR nms_ne_id) with commands[].",
items: {
type: "object",
additionalProperties: false,
@ -2643,8 +2725,11 @@ function registerNetxTools(ctx, connection, options) {
commands: strArr("Commands for single NE, or shared commands for batch."),
read_timeout_sec: num4("Per-command read timeout (default 60; use 90–120 for slow show)."),
concurrency: num4("Parallel NEs for batch mode (1–8, default 4)."),
async: bool4("oclaw-only async hint; ignored by native REST client.")
async: bool4("true=background job_id + getNeExecJob; false=force sync; omit=auto for ~4+ NEs (NETX_NE_EXEC_ASYNC_MIN_NES).")
}, execManagedNe, getClient, Math.max(t, 300000)),
tool2("netx__getNeExecJob", "Poll a background execManagedNe job (job_id from async ack). When terminal=true, result holds exec/exec-batch payload. Do not busy-wait in the same turn.", {
job_id: str4("Job id from execManagedNe async response.")
}, getNeExecJob, getClient, Math.max(t, 60000)),
tool2("netx__listCliTargets", "List CLI-capable targets (managed NE and/or NMS inventory). Call once per session with keyword/source, cache ids, then execManagedNe.", {
source: str4("managed | nms | ume | all"),
keyword: str4(),

View file

@ -18,6 +18,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
"netx__getNeExecJob",
"netx__listCliTargets",
"netx__findTopologyPaths"
]),
@ -1133,7 +1134,60 @@ function quoteNeId(neId) {
}
// src/netx/handlers.ts
var EXEC_MAX_COMMANDS = 5;
var EXEC_MAX_COMMANDS_CAP = 50;
var EXEC_MAX_COMMANDS_DEFAULT = 5;
function execMaxCommands() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_MAX_COMMANDS ?? ""), 10);
const n = Number.isFinite(raw) ? raw : EXEC_MAX_COMMANDS_DEFAULT;
return Math.max(1, Math.min(EXEC_MAX_COMMANDS_CAP, n));
}
function asyncMinNes() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_ASYNC_MIN_NES ?? ""), 10);
const n = Number.isFinite(raw) ? raw : 4;
return Math.max(0, Math.min(50, n));
}
function truthyAsyncFlag(raw) {
if (raw === undefined || raw === null)
return;
if (typeof raw === "boolean")
return raw;
const text = String(raw).trim().toLowerCase();
if (!text)
return;
if (["1", "true", "yes", "on"].includes(text))
return true;
if (["0", "false", "no", "off"].includes(text))
return false;
return;
}
function countExecNeTargets(args) {
let n = 0;
for (const key of ["ne_ids", "nms_ne_ids", "ume_ne_ids"]) {
const val = strList(args, key);
if (val.length > 0)
n = Math.max(n, val.length);
}
const targets = args.targets;
if (Array.isArray(targets)) {
n = Math.max(n, targets.filter((t) => typeof t === "object" && t !== null && !Array.isArray(t)).length);
}
if (n === 0) {
if (str2(args, "ne_id").trim() || nmsOrUme(args, "nms_ne_id", "ume_ne_id"))
return 1;
}
return n;
}
function shouldRunExecAsync(args) {
const flag = truthyAsyncFlag(args.async);
if (flag === false)
return false;
if (flag === true)
return true;
const minN = asyncMinNes();
if (minN <= 0)
return false;
return countExecNeTargets(args) >= minN;
}
var UME_RAW_FIELD_PRESETS = {
brief: [
"alarm_alarm_key",
@ -1375,6 +1429,8 @@ async function getManagedNe(client, args, signal) {
return out;
}
async function execManagedNe(client, args, signal) {
const maxCmds = execMaxCommands();
const wantAsync = shouldRunExecAsync(args);
const targetsRaw = args.targets;
const neIds = strList(args, "ne_ids");
const umeNeIds = nmsOrUmeList(args, "nms_ne_ids", "ume_ne_ids");
@ -1408,7 +1464,7 @@ async function execManagedNe(client, args, signal) {
if (umeNeIds.length > 0)
body2.ume_ne_ids = umeNeIds;
if (sharedCommands.length > 0) {
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
body2.commands = sharedCommands;
@ -1417,6 +1473,13 @@ async function execManagedNe(client, args, signal) {
const concurrency = num2(args, "concurrency");
if (concurrency !== undefined)
body2.concurrency = clampInt(concurrency, 4, 1, 8);
if (wantAsync) {
const out3 = await client.post("/v1/managed-ne/exec-jobs", body2, signal, 60000);
if (out3.ok !== true)
return out3;
const data3 = asRecord(out3.data);
return Object.keys(data3).length > 0 ? data3 : out3;
}
const out2 = await client.post("/v1/managed-ne/exec-batch", body2, signal, 600000);
if (out2.ok !== true)
return out2;
@ -1439,7 +1502,7 @@ async function execManagedNe(client, args, signal) {
if (sharedCommands.length === 0) {
return { ok: false, error: "commands_required", error_code: "commands_required" };
}
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
const body = {
@ -1450,6 +1513,13 @@ async function execManagedNe(client, args, signal) {
body.ne_id = neId;
if (umeNeId)
body.ume_ne_id = umeNeId;
if (wantAsync) {
const out2 = await client.post("/v1/managed-ne/exec-jobs", body, signal, 60000);
if (out2.ok !== true)
return out2;
const data2 = asRecord(out2.data);
return Object.keys(data2).length > 0 ? data2 : out2;
}
const out = await client.post("/v1/managed-ne/exec", body, signal, 300000);
if (out.ok !== true)
return out;
@ -1459,6 +1529,18 @@ async function execManagedNe(client, args, signal) {
}
return { ok: true, data };
}
async function getNeExecJob(client, args, signal) {
const jobId = str2(args, "job_id").trim();
if (!jobId) {
return {
ok: false,
error: "job_id_required",
error_code: "job_id_required",
hint: "Pass job_id from execManagedNe async ack."
};
}
return client.get(`/v1/managed-ne/exec-jobs/${quoteNeId(jobId)}`, undefined, signal);
}
async function listCliTargets(client, args, signal) {
const params = {
page: clampInt(num2(args, "page"), 1, 1, Number.MAX_SAFE_INTEGER),
@ -2614,12 +2696,12 @@ function registerNetxTools(ctx, connection, options) {
page: num4(),
page_size: num4()
}, listManagedNe, getClient, t),
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Do NOT pass NMS inventory UUID here.", {
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Response includes capability (device_family, exec_policy_effective, recommended_mode, hints) — read before complex execManagedNe. Do NOT pass NMS inventory UUID here.", {
ne_id: str4("Managed NE id"),
managed_ne_id: str4("Alias for ne_id"),
id: str4("Alias for ne_id")
}, getManagedNe, getClient, t),
tool2("netx__execManagedNe", "Run read-only CLI via netx (show/display/ping/traceroute). Single NE: ne_id OR ume_ne_id + commands. Many NEs: ne_ids[]/ume_ne_ids[] + shared commands, or targets[{ume_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work.", {
tool2("netx__execManagedNe", "Run CLI via netx (readonly show/display/ping/traceroute; linux_shell on Linux/MikroTik allows scripts). Single NE: ne_id OR nms_ne_id + commands. Many NEs: ne_ids[]/nms_ne_ids[] + shared commands, or targets[{nms_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work. Long or multi-NE: async=true (or auto when ≥4 NEs) returns job_id — poll getNeExecJob. Read getManagedNe.capability first.", {
ne_id: str4(),
nms_ne_id: str4("NMS inventory id; alias ume_ne_id"),
ume_ne_id: str4("Legacy alias of nms_ne_id"),
@ -2628,7 +2710,7 @@ function registerNetxTools(ctx, connection, options) {
ume_ne_ids: strArr("Legacy alias of nms_ne_ids"),
targets: {
type: "array",
description: "Per-NE command sets: each item is one NE (ne_id OR ume_ne_id) with commands[].",
description: "Per-NE command sets: each item is one NE (ne_id OR nms_ne_id) with commands[].",
items: {
type: "object",
additionalProperties: false,
@ -2643,8 +2725,11 @@ function registerNetxTools(ctx, connection, options) {
commands: strArr("Commands for single NE, or shared commands for batch."),
read_timeout_sec: num4("Per-command read timeout (default 60; use 90–120 for slow show)."),
concurrency: num4("Parallel NEs for batch mode (1–8, default 4)."),
async: bool4("oclaw-only async hint; ignored by native REST client.")
async: bool4("true=background job_id + getNeExecJob; false=force sync; omit=auto for ~4+ NEs (NETX_NE_EXEC_ASYNC_MIN_NES).")
}, execManagedNe, getClient, Math.max(t, 300000)),
tool2("netx__getNeExecJob", "Poll a background execManagedNe job (job_id from async ack). When terminal=true, result holds exec/exec-batch payload. Do not busy-wait in the same turn.", {
job_id: str4("Job id from execManagedNe async response.")
}, getNeExecJob, getClient, Math.max(t, 60000)),
tool2("netx__listCliTargets", "List CLI-capable targets (managed NE and/or NMS inventory). Call once per session with keyword/source, cache ids, then execManagedNe.", {
source: str4("managed | nms | ume | all"),
keyword: str4(),

View file

@ -18,6 +18,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
"netx__getNeExecJob",
"netx__listCliTargets",
"netx__findTopologyPaths"
]),
@ -1133,7 +1134,60 @@ function quoteNeId(neId) {
}
// src/netx/handlers.ts
var EXEC_MAX_COMMANDS = 5;
var EXEC_MAX_COMMANDS_CAP = 50;
var EXEC_MAX_COMMANDS_DEFAULT = 5;
function execMaxCommands() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_MAX_COMMANDS ?? ""), 10);
const n = Number.isFinite(raw) ? raw : EXEC_MAX_COMMANDS_DEFAULT;
return Math.max(1, Math.min(EXEC_MAX_COMMANDS_CAP, n));
}
function asyncMinNes() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_ASYNC_MIN_NES ?? ""), 10);
const n = Number.isFinite(raw) ? raw : 4;
return Math.max(0, Math.min(50, n));
}
function truthyAsyncFlag(raw) {
if (raw === undefined || raw === null)
return;
if (typeof raw === "boolean")
return raw;
const text = String(raw).trim().toLowerCase();
if (!text)
return;
if (["1", "true", "yes", "on"].includes(text))
return true;
if (["0", "false", "no", "off"].includes(text))
return false;
return;
}
function countExecNeTargets(args) {
let n = 0;
for (const key of ["ne_ids", "nms_ne_ids", "ume_ne_ids"]) {
const val = strList(args, key);
if (val.length > 0)
n = Math.max(n, val.length);
}
const targets = args.targets;
if (Array.isArray(targets)) {
n = Math.max(n, targets.filter((t) => typeof t === "object" && t !== null && !Array.isArray(t)).length);
}
if (n === 0) {
if (str2(args, "ne_id").trim() || nmsOrUme(args, "nms_ne_id", "ume_ne_id"))
return 1;
}
return n;
}
function shouldRunExecAsync(args) {
const flag = truthyAsyncFlag(args.async);
if (flag === false)
return false;
if (flag === true)
return true;
const minN = asyncMinNes();
if (minN <= 0)
return false;
return countExecNeTargets(args) >= minN;
}
var UME_RAW_FIELD_PRESETS = {
brief: [
"alarm_alarm_key",
@ -1375,6 +1429,8 @@ async function getManagedNe(client, args, signal) {
return out;
}
async function execManagedNe(client, args, signal) {
const maxCmds = execMaxCommands();
const wantAsync = shouldRunExecAsync(args);
const targetsRaw = args.targets;
const neIds = strList(args, "ne_ids");
const umeNeIds = nmsOrUmeList(args, "nms_ne_ids", "ume_ne_ids");
@ -1408,7 +1464,7 @@ async function execManagedNe(client, args, signal) {
if (umeNeIds.length > 0)
body2.ume_ne_ids = umeNeIds;
if (sharedCommands.length > 0) {
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
body2.commands = sharedCommands;
@ -1417,6 +1473,13 @@ async function execManagedNe(client, args, signal) {
const concurrency = num2(args, "concurrency");
if (concurrency !== undefined)
body2.concurrency = clampInt(concurrency, 4, 1, 8);
if (wantAsync) {
const out3 = await client.post("/v1/managed-ne/exec-jobs", body2, signal, 60000);
if (out3.ok !== true)
return out3;
const data3 = asRecord(out3.data);
return Object.keys(data3).length > 0 ? data3 : out3;
}
const out2 = await client.post("/v1/managed-ne/exec-batch", body2, signal, 600000);
if (out2.ok !== true)
return out2;
@ -1439,7 +1502,7 @@ async function execManagedNe(client, args, signal) {
if (sharedCommands.length === 0) {
return { ok: false, error: "commands_required", error_code: "commands_required" };
}
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
const body = {
@ -1450,6 +1513,13 @@ async function execManagedNe(client, args, signal) {
body.ne_id = neId;
if (umeNeId)
body.ume_ne_id = umeNeId;
if (wantAsync) {
const out2 = await client.post("/v1/managed-ne/exec-jobs", body, signal, 60000);
if (out2.ok !== true)
return out2;
const data2 = asRecord(out2.data);
return Object.keys(data2).length > 0 ? data2 : out2;
}
const out = await client.post("/v1/managed-ne/exec", body, signal, 300000);
if (out.ok !== true)
return out;
@ -1459,6 +1529,18 @@ async function execManagedNe(client, args, signal) {
}
return { ok: true, data };
}
async function getNeExecJob(client, args, signal) {
const jobId = str2(args, "job_id").trim();
if (!jobId) {
return {
ok: false,
error: "job_id_required",
error_code: "job_id_required",
hint: "Pass job_id from execManagedNe async ack."
};
}
return client.get(`/v1/managed-ne/exec-jobs/${quoteNeId(jobId)}`, undefined, signal);
}
async function listCliTargets(client, args, signal) {
const params = {
page: clampInt(num2(args, "page"), 1, 1, Number.MAX_SAFE_INTEGER),
@ -2614,12 +2696,12 @@ function registerNetxTools(ctx, connection, options) {
page: num4(),
page_size: num4()
}, listManagedNe, getClient, t),
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Do NOT pass NMS inventory UUID here.", {
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Response includes capability (device_family, exec_policy_effective, recommended_mode, hints) — read before complex execManagedNe. Do NOT pass NMS inventory UUID here.", {
ne_id: str4("Managed NE id"),
managed_ne_id: str4("Alias for ne_id"),
id: str4("Alias for ne_id")
}, getManagedNe, getClient, t),
tool2("netx__execManagedNe", "Run read-only CLI via netx (show/display/ping/traceroute). Single NE: ne_id OR ume_ne_id + commands. Many NEs: ne_ids[]/ume_ne_ids[] + shared commands, or targets[{ume_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work.", {
tool2("netx__execManagedNe", "Run CLI via netx (readonly show/display/ping/traceroute; linux_shell on Linux/MikroTik allows scripts). Single NE: ne_id OR nms_ne_id + commands. Many NEs: ne_ids[]/nms_ne_ids[] + shared commands, or targets[{nms_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work. Long or multi-NE: async=true (or auto when ≥4 NEs) returns job_id — poll getNeExecJob. Read getManagedNe.capability first.", {
ne_id: str4(),
nms_ne_id: str4("NMS inventory id; alias ume_ne_id"),
ume_ne_id: str4("Legacy alias of nms_ne_id"),
@ -2628,7 +2710,7 @@ function registerNetxTools(ctx, connection, options) {
ume_ne_ids: strArr("Legacy alias of nms_ne_ids"),
targets: {
type: "array",
description: "Per-NE command sets: each item is one NE (ne_id OR ume_ne_id) with commands[].",
description: "Per-NE command sets: each item is one NE (ne_id OR nms_ne_id) with commands[].",
items: {
type: "object",
additionalProperties: false,
@ -2643,8 +2725,11 @@ function registerNetxTools(ctx, connection, options) {
commands: strArr("Commands for single NE, or shared commands for batch."),
read_timeout_sec: num4("Per-command read timeout (default 60; use 90–120 for slow show)."),
concurrency: num4("Parallel NEs for batch mode (1–8, default 4)."),
async: bool4("oclaw-only async hint; ignored by native REST client.")
async: bool4("true=background job_id + getNeExecJob; false=force sync; omit=auto for ~4+ NEs (NETX_NE_EXEC_ASYNC_MIN_NES).")
}, execManagedNe, getClient, Math.max(t, 300000)),
tool2("netx__getNeExecJob", "Poll a background execManagedNe job (job_id from async ack). When terminal=true, result holds exec/exec-batch payload. Do not busy-wait in the same turn.", {
job_id: str4("Job id from execManagedNe async response.")
}, getNeExecJob, getClient, Math.max(t, 60000)),
tool2("netx__listCliTargets", "List CLI-capable targets (managed NE and/or NMS inventory). Call once per session with keyword/source, cache ids, then execManagedNe.", {
source: str4("managed | nms | ume | all"),
keyword: str4(),

View file

@ -589,6 +589,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
"netx__getNeExecJob",
"netx__listCliTargets",
"netx__findTopologyPaths"
]),
@ -3392,7 +3393,60 @@ function quoteNeId(neId) {
}
// src/netx/handlers.ts
var EXEC_MAX_COMMANDS = 5;
var EXEC_MAX_COMMANDS_CAP = 50;
var EXEC_MAX_COMMANDS_DEFAULT = 5;
function execMaxCommands() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_MAX_COMMANDS ?? ""), 10);
const n = Number.isFinite(raw) ? raw : EXEC_MAX_COMMANDS_DEFAULT;
return Math.max(1, Math.min(EXEC_MAX_COMMANDS_CAP, n));
}
function asyncMinNes() {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_ASYNC_MIN_NES ?? ""), 10);
const n = Number.isFinite(raw) ? raw : 4;
return Math.max(0, Math.min(50, n));
}
function truthyAsyncFlag(raw) {
if (raw === undefined || raw === null)
return;
if (typeof raw === "boolean")
return raw;
const text = String(raw).trim().toLowerCase();
if (!text)
return;
if (["1", "true", "yes", "on"].includes(text))
return true;
if (["0", "false", "no", "off"].includes(text))
return false;
return;
}
function countExecNeTargets(args) {
let n = 0;
for (const key of ["ne_ids", "nms_ne_ids", "ume_ne_ids"]) {
const val = strList(args, key);
if (val.length > 0)
n = Math.max(n, val.length);
}
const targets = args.targets;
if (Array.isArray(targets)) {
n = Math.max(n, targets.filter((t) => typeof t === "object" && t !== null && !Array.isArray(t)).length);
}
if (n === 0) {
if (str2(args, "ne_id").trim() || nmsOrUme(args, "nms_ne_id", "ume_ne_id"))
return 1;
}
return n;
}
function shouldRunExecAsync(args) {
const flag = truthyAsyncFlag(args.async);
if (flag === false)
return false;
if (flag === true)
return true;
const minN = asyncMinNes();
if (minN <= 0)
return false;
return countExecNeTargets(args) >= minN;
}
var UME_RAW_FIELD_PRESETS = {
brief: [
"alarm_alarm_key",
@ -3634,6 +3688,8 @@ async function getManagedNe(client, args, signal) {
return out;
}
async function execManagedNe(client, args, signal) {
const maxCmds = execMaxCommands();
const wantAsync = shouldRunExecAsync(args);
const targetsRaw = args.targets;
const neIds = strList(args, "ne_ids");
const umeNeIds = nmsOrUmeList(args, "nms_ne_ids", "ume_ne_ids");
@ -3667,7 +3723,7 @@ async function execManagedNe(client, args, signal) {
if (umeNeIds.length > 0)
body2.ume_ne_ids = umeNeIds;
if (sharedCommands.length > 0) {
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
body2.commands = sharedCommands;
@ -3676,6 +3732,13 @@ async function execManagedNe(client, args, signal) {
const concurrency = num2(args, "concurrency");
if (concurrency !== undefined)
body2.concurrency = clampInt(concurrency, 4, 1, 8);
if (wantAsync) {
const out3 = await client.post("/v1/managed-ne/exec-jobs", body2, signal, 60000);
if (out3.ok !== true)
return out3;
const data3 = asRecord(out3.data);
return Object.keys(data3).length > 0 ? data3 : out3;
}
const out2 = await client.post("/v1/managed-ne/exec-batch", body2, signal, 600000);
if (out2.ok !== true)
return out2;
@ -3698,7 +3761,7 @@ async function execManagedNe(client, args, signal) {
if (sharedCommands.length === 0) {
return { ok: false, error: "commands_required", error_code: "commands_required" };
}
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: "too_many_commands", error_code: "too_many_commands" };
}
const body = {
@ -3709,6 +3772,13 @@ async function execManagedNe(client, args, signal) {
body.ne_id = neId;
if (umeNeId)
body.ume_ne_id = umeNeId;
if (wantAsync) {
const out2 = await client.post("/v1/managed-ne/exec-jobs", body, signal, 60000);
if (out2.ok !== true)
return out2;
const data2 = asRecord(out2.data);
return Object.keys(data2).length > 0 ? data2 : out2;
}
const out = await client.post("/v1/managed-ne/exec", body, signal, 300000);
if (out.ok !== true)
return out;
@ -3718,6 +3788,18 @@ async function execManagedNe(client, args, signal) {
}
return { ok: true, data };
}
async function getNeExecJob(client, args, signal) {
const jobId = str2(args, "job_id").trim();
if (!jobId) {
return {
ok: false,
error: "job_id_required",
error_code: "job_id_required",
hint: "Pass job_id from execManagedNe async ack."
};
}
return client.get(`/v1/managed-ne/exec-jobs/${quoteNeId(jobId)}`, undefined, signal);
}
async function listCliTargets(client, args, signal) {
const params = {
page: clampInt(num2(args, "page"), 1, 1, Number.MAX_SAFE_INTEGER),
@ -4873,12 +4955,12 @@ function registerNetxTools(ctx, connection, options) {
page: num4(),
page_size: num4()
}, listManagedNe, getClient, t),
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Do NOT pass NMS inventory UUID here.", {
tool2("netx__getManagedNe", "Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Response includes capability (device_family, exec_policy_effective, recommended_mode, hints) — read before complex execManagedNe. Do NOT pass NMS inventory UUID here.", {
ne_id: str4("Managed NE id"),
managed_ne_id: str4("Alias for ne_id"),
id: str4("Alias for ne_id")
}, getManagedNe, getClient, t),
tool2("netx__execManagedNe", "Run read-only CLI via netx (show/display/ping/traceroute). Single NE: ne_id OR ume_ne_id + commands. Many NEs: ne_ids[]/ume_ne_ids[] + shared commands, or targets[{ume_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work.", {
tool2("netx__execManagedNe", "Run CLI via netx (readonly show/display/ping/traceroute; linux_shell on Linux/MikroTik allows scripts). Single NE: ne_id OR nms_ne_id + commands. Many NEs: ne_ids[]/nms_ne_ids[] + shared commands, or targets[{nms_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work. Long or multi-NE: async=true (or auto when ≥4 NEs) returns job_id — poll getNeExecJob. Read getManagedNe.capability first.", {
ne_id: str4(),
nms_ne_id: str4("NMS inventory id; alias ume_ne_id"),
ume_ne_id: str4("Legacy alias of nms_ne_id"),
@ -4887,7 +4969,7 @@ function registerNetxTools(ctx, connection, options) {
ume_ne_ids: strArr("Legacy alias of nms_ne_ids"),
targets: {
type: "array",
description: "Per-NE command sets: each item is one NE (ne_id OR ume_ne_id) with commands[].",
description: "Per-NE command sets: each item is one NE (ne_id OR nms_ne_id) with commands[].",
items: {
type: "object",
additionalProperties: false,
@ -4902,8 +4984,11 @@ function registerNetxTools(ctx, connection, options) {
commands: strArr("Commands for single NE, or shared commands for batch."),
read_timeout_sec: num4("Per-command read timeout (default 60; use 90–120 for slow show)."),
concurrency: num4("Parallel NEs for batch mode (1–8, default 4)."),
async: bool4("oclaw-only async hint; ignored by native REST client.")
async: bool4("true=background job_id + getNeExecJob; false=force sync; omit=auto for ~4+ NEs (NETX_NE_EXEC_ASYNC_MIN_NES).")
}, execManagedNe, getClient, Math.max(t, 300000)),
tool2("netx__getNeExecJob", "Poll a background execManagedNe job (job_id from async ack). When terminal=true, result holds exec/exec-batch payload. Do not busy-wait in the same turn.", {
job_id: str4("Job id from execManagedNe async response.")
}, getNeExecJob, getClient, Math.max(t, 60000)),
tool2("netx__listCliTargets", "List CLI-capable targets (managed NE and/or NMS inventory). Call once per session with keyword/source, cache ids, then execManagedNe.", {
source: str4("managed | nms | ume | all"),
keyword: str4(),

View file

@ -1,6 +1,6 @@
{
"name": "dsh-netxops",
"version": "0.1.40",
"version": "0.1.41",
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
"license": "MIT",
"type": "module",

View file

@ -29,6 +29,8 @@
| 证据 | `queryNmsAlarmsRaw(field_preset=evidence)` |
| 清单 | `queryNmsNeInventory` / `getNmsNe` |
| 多台 CLI | `execManagedNe(nms_ne_ids=…)` 或 `targets` |
| 长任务 / ≥4 台 | `execManagedNe(async=true)` → `getNeExecJob(job_id=…)` |
| 设备能力 | `getManagedNe` → 读 `capability.recommended_mode` |
| 路径 | `findTopologyPaths` |
| A<>B 光 | 两端 host → 路径 → 多台一批 optic CLI |

View file

@ -59,6 +59,7 @@ export const TOOLS_BY_GROUP: Readonly<Record<NetxCapabilityGroupId, readonly str
'netx__listManagedNe',
'netx__getManagedNe',
'netx__execManagedNe',
'netx__getNeExecJob',
'netx__listCliTargets',
'netx__findTopologyPaths',
]),

View file

@ -5,7 +5,55 @@
import { type NetxClient, quoteNeId, type NetxJson } from './http.ts'
import { omitUndefined } from './json-safe.ts'
const EXEC_MAX_COMMANDS = 5
const EXEC_MAX_COMMANDS_CAP = 50
const EXEC_MAX_COMMANDS_DEFAULT = 5
function execMaxCommands(): number {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_MAX_COMMANDS ?? ''), 10)
const n = Number.isFinite(raw) ? raw : EXEC_MAX_COMMANDS_DEFAULT
return Math.max(1, Math.min(EXEC_MAX_COMMANDS_CAP, n))
}
function asyncMinNes(): number {
const raw = Number.parseInt(String(process.env.NETX_NE_EXEC_ASYNC_MIN_NES ?? ''), 10)
const n = Number.isFinite(raw) ? raw : 4
return Math.max(0, Math.min(50, n))
}
function truthyAsyncFlag(raw: unknown): boolean | undefined {
if (raw === undefined || raw === null) return undefined
if (typeof raw === 'boolean') return raw
const text = String(raw).trim().toLowerCase()
if (!text) return undefined
if (['1', 'true', 'yes', 'on'].includes(text)) return true
if (['0', 'false', 'no', 'off'].includes(text)) return false
return undefined
}
function countExecNeTargets(args: NetxJson): number {
let n = 0
for (const key of ['ne_ids', 'nms_ne_ids', 'ume_ne_ids'] as const) {
const val = strList(args, key)
if (val.length > 0) n = Math.max(n, val.length)
}
const targets = args.targets
if (Array.isArray(targets)) {
n = Math.max(n, targets.filter(t => typeof t === 'object' && t !== null && !Array.isArray(t)).length)
}
if (n === 0) {
if (str(args, 'ne_id').trim() || nmsOrUme(args, 'nms_ne_id', 'ume_ne_id')) return 1
}
return n
}
function shouldRunExecAsync(args: NetxJson): boolean {
const flag = truthyAsyncFlag(args.async)
if (flag === false) return false
if (flag === true) return true
const minN = asyncMinNes()
if (minN <= 0) return false
return countExecNeTargets(args) >= minN
}
const UME_RAW_FIELD_PRESETS: Record<string, string[]> = {
brief: [
@ -218,6 +266,8 @@ export async function getManagedNe(client: NetxClient, args: NetxJson, signal?:
}
export async function execManagedNe(client: NetxClient, args: NetxJson, signal?: AbortSignal): Promise<NetxJson> {
const maxCmds = execMaxCommands()
const wantAsync = shouldRunExecAsync(args)
const targetsRaw = args.targets
const neIds = strList(args, 'ne_ids')
const umeNeIds = nmsOrUmeList(args, 'nms_ne_ids', 'ume_ne_ids')
@ -249,7 +299,7 @@ export async function execManagedNe(client: NetxClient, args: NetxJson, signal?:
if (neIds.length > 0) body.ne_ids = neIds
if (umeNeIds.length > 0) body.ume_ne_ids = umeNeIds
if (sharedCommands.length > 0) {
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: 'too_many_commands', error_code: 'too_many_commands' }
}
body.commands = sharedCommands
@ -257,6 +307,12 @@ export async function execManagedNe(client: NetxClient, args: NetxJson, signal?:
body.read_timeout_sec = clampInt(num(args, 'read_timeout_sec'), 60, 10, 120)
const concurrency = num(args, 'concurrency')
if (concurrency !== undefined) body.concurrency = clampInt(concurrency, 4, 1, 8)
if (wantAsync) {
const out = await client.post('/v1/managed-ne/exec-jobs', body, signal, 60_000)
if (out.ok !== true) return out
const data = asRecord(out.data)
return Object.keys(data).length > 0 ? data : out
}
const out = await client.post('/v1/managed-ne/exec-batch', body, signal, 600_000)
if (out.ok !== true) return out
const data = asRecord(out.data)
@ -279,7 +335,7 @@ export async function execManagedNe(client: NetxClient, args: NetxJson, signal?:
if (sharedCommands.length === 0) {
return { ok: false, error: 'commands_required', error_code: 'commands_required' }
}
if (sharedCommands.length > EXEC_MAX_COMMANDS) {
if (sharedCommands.length > maxCmds) {
return { ok: false, error: 'too_many_commands', error_code: 'too_many_commands' }
}
const body: NetxJson = {
@ -288,6 +344,12 @@ export async function execManagedNe(client: NetxClient, args: NetxJson, signal?:
}
if (neId) body.ne_id = neId
if (umeNeId) body.ume_ne_id = umeNeId
if (wantAsync) {
const out = await client.post('/v1/managed-ne/exec-jobs', body, signal, 60_000)
if (out.ok !== true) return out
const data = asRecord(out.data)
return Object.keys(data).length > 0 ? data : out
}
const out = await client.post('/v1/managed-ne/exec', body, signal, 300_000)
if (out.ok !== true) return out
const data = asRecord(out.data)
@ -297,6 +359,19 @@ export async function execManagedNe(client: NetxClient, args: NetxJson, signal?:
return { ok: true, data }
}
export async function getNeExecJob(client: NetxClient, args: NetxJson, signal?: AbortSignal): Promise<NetxJson> {
const jobId = str(args, 'job_id').trim()
if (!jobId) {
return {
ok: false,
error: 'job_id_required',
error_code: 'job_id_required',
hint: 'Pass job_id from execManagedNe async ack.',
}
}
return client.get(`/v1/managed-ne/exec-jobs/${quoteNeId(jobId)}`, undefined, signal)
}
export async function listCliTargets(client: NetxClient, args: NetxJson, signal?: AbortSignal): Promise<NetxJson> {
const params: Record<string, string | number | boolean> = {
page: clampInt(num(args, 'page'), 1, 1, Number.MAX_SAFE_INTEGER),

View file

@ -253,7 +253,7 @@ export function registerNetxTools(
),
tool(
'netx__getManagedNe',
'Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Do NOT pass NMS inventory UUID here.',
'Get one managed NE by managed ne_id (from listManagedNe / listCliTargets source=managed). Response includes capability (device_family, exec_policy_effective, recommended_mode, hints) — read before complex execManagedNe. Do NOT pass NMS inventory UUID here.',
{
ne_id: str('Managed NE id'),
managed_ne_id: str('Alias for ne_id'),
@ -263,7 +263,7 @@ export function registerNetxTools(
),
tool(
'netx__execManagedNe',
'Run read-only CLI via netx (show/display/ping/traceroute). Single NE: ne_id OR ume_ne_id + commands. Many NEs: ne_ids[]/ume_ne_ids[] + shared commands, or targets[{ume_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work.',
'Run CLI via netx (readonly show/display/ping/traceroute; linux_shell on Linux/MikroTik allows scripts). Single NE: ne_id OR nms_ne_id + commands. Many NEs: ne_ids[]/nms_ne_ids[] + shared commands, or targets[{nms_ne_id|ne_id, commands}]. Do NOT loop one-NE calls for multi-NE work. Long or multi-NE: async=true (or auto when ≥4 NEs) returns job_id — poll getNeExecJob. Read getManagedNe.capability first.',
{
ne_id: str(),
nms_ne_id: str('NMS inventory id; alias ume_ne_id'),
@ -273,7 +273,7 @@ export function registerNetxTools(
ume_ne_ids: strArr('Legacy alias of nms_ne_ids'),
targets: {
type: 'array' as const,
description: 'Per-NE command sets: each item is one NE (ne_id OR ume_ne_id) with commands[].',
description: 'Per-NE command sets: each item is one NE (ne_id OR nms_ne_id) with commands[].',
items: {
type: 'object' as const,
additionalProperties: false,
@ -288,10 +288,18 @@ export function registerNetxTools(
commands: strArr('Commands for single NE, or shared commands for batch.'),
read_timeout_sec: num('Per-command read timeout (default 60; use 90–120 for slow show).'),
concurrency: num('Parallel NEs for batch mode (1–8, default 4).'),
async: bool('oclaw-only async hint; ignored by native REST client.'),
async: bool('true=background job_id + getNeExecJob; false=force sync; omit=auto for ~4+ NEs (NETX_NE_EXEC_ASYNC_MIN_NES).'),
},
H.execManagedNe, getClient, Math.max(t, 300_000),
),
tool(
'netx__getNeExecJob',
'Poll a background execManagedNe job (job_id from async ack). When terminal=true, result holds exec/exec-batch payload. Do not busy-wait in the same turn.',
{
job_id: str('Job id from execManagedNe async response.'),
},
H.getNeExecJob, getClient, Math.max(t, 60_000),
),
tool(
'netx__listCliTargets',
'List CLI-capable targets (managed NE and/or NMS inventory). Call once per session with keyword/source, cache ids, then execManagedNe.',

View file

@ -33,6 +33,7 @@ test('one group one skill dir; ops owns NMS + managed CLI; topology owns canvas;
})
assert.ok(TOOLS_BY_GROUP.ops.includes('netx__findTopologyPaths'))
assert.ok(TOOLS_BY_GROUP.ops.includes('netx__execManagedNe'))
assert.ok(TOOLS_BY_GROUP.ops.includes('netx__getNeExecJob'))
assert.ok(TOOLS_BY_GROUP.ops.includes('netx__queryNmsAlarms'))
assert.ok(TOOLS_BY_GROUP.topology.includes('netx__getTopologyTree'))
assert.ok(TOOLS_BY_GROUP.topology.includes('netx__layoutTopologyView'))