mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 08:00:45 +08:00
Harden alarm push auth and session export ZIP paths.
Ignore hub alarms before auth-ok, stop reconnecting after auth-fail, serialize sticky delivery with dispose on reset, and sanitize ZIP entry names against path traversal. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
92279c1220
commit
5598b3661c
5 changed files with 148 additions and 56 deletions
89
lib/index.js
89
lib/index.js
|
|
@ -148,6 +148,8 @@ function startAlarmPushClient(options) {
|
|||
return () => {};
|
||||
}
|
||||
let closed = false;
|
||||
let haltReconnect = false;
|
||||
let subscribed = false;
|
||||
let socket = null;
|
||||
let reconnectTimer;
|
||||
let attempt = 0;
|
||||
|
|
@ -159,7 +161,7 @@ function startAlarmPushClient(options) {
|
|||
}
|
||||
};
|
||||
const scheduleReconnect = (reason) => {
|
||||
if (closed)
|
||||
if (closed || haltReconnect)
|
||||
return;
|
||||
clearReconnect();
|
||||
const delay = Math.min(60000, baseDelay * 2 ** Math.min(attempt, 5));
|
||||
|
|
@ -173,9 +175,10 @@ function startAlarmPushClient(options) {
|
|||
}, delay);
|
||||
};
|
||||
const connect = () => {
|
||||
if (closed)
|
||||
if (closed || haltReconnect)
|
||||
return;
|
||||
clearReconnect();
|
||||
subscribed = false;
|
||||
setPhase(attempt > 0 ? "reconnecting" : "connecting", wsUrl, { detail: "dialing" });
|
||||
try {
|
||||
socket = new WS(wsUrl);
|
||||
|
|
@ -199,6 +202,7 @@ function startAlarmPushClient(options) {
|
|||
const type = String(msg.type ?? "").toLowerCase();
|
||||
if (type === "auth-ok") {
|
||||
attempt = 0;
|
||||
subscribed = true;
|
||||
const now = Date.now();
|
||||
setPhase("connected", wsUrl, {
|
||||
detail: String(msg.user ?? "ok"),
|
||||
|
|
@ -210,14 +214,23 @@ function startAlarmPushClient(options) {
|
|||
}
|
||||
if (type === "auth-fail") {
|
||||
const err = String(msg.error ?? "auth_failed");
|
||||
log.error?.("netxops alarm-push: auth failed (%s)", err);
|
||||
log.error?.("netxops alarm-push: auth failed (%s) — not reconnecting until settings/token change", err);
|
||||
subscribed = false;
|
||||
haltReconnect = true;
|
||||
clearReconnect();
|
||||
setPhase("auth_failed", wsUrl, { detail: err, lastError: err });
|
||||
socket?.close();
|
||||
try {
|
||||
socket?.close();
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
if (type === "pong")
|
||||
return;
|
||||
if (type === "event" && String(msg.event ?? "") === "netx.alarm") {
|
||||
if (!subscribed) {
|
||||
log.warn?.("netxops alarm-push: ignoring alarm before auth-ok");
|
||||
return;
|
||||
}
|
||||
const payload = msg.payload && typeof msg.payload === "object" ? msg.payload : {};
|
||||
Promise.resolve(options.onAlarm(payload)).catch((error) => {
|
||||
log.warn?.("netxops alarm-push: handler failed:", error);
|
||||
|
|
@ -226,7 +239,8 @@ function startAlarmPushClient(options) {
|
|||
});
|
||||
socket.addEventListener("close", () => {
|
||||
socket = null;
|
||||
if (!closed)
|
||||
subscribed = false;
|
||||
if (!closed && !haltReconnect)
|
||||
scheduleReconnect("socket_closed");
|
||||
});
|
||||
socket.addEventListener("error", () => {});
|
||||
|
|
@ -356,28 +370,47 @@ var PRESET_ID = "netxops";
|
|||
var PERMISSION_PRESET = "default";
|
||||
var TITLE = "Netx 关键告警";
|
||||
var sticky = null;
|
||||
var deliveryChain = Promise.resolve();
|
||||
function resolveWorkspacePath() {
|
||||
const fromEnv = process.env.DSH_HOME?.trim();
|
||||
const home = fromEnv && fromEnv.length > 0 ? fromEnv : join(homedir(), ".dsh");
|
||||
return join(home, "workspaces", "netxops-alarms");
|
||||
}
|
||||
async function deliverAlarmToSession(ctx, payload, lang = "zh") {
|
||||
const prompt = formatAlarmPrompt(payload, lang);
|
||||
const agents = ctx.agents;
|
||||
if (!agents || typeof agents.create !== "function") {
|
||||
ctx.logger.warn("netxops alarm-push: ctx.agents unavailable — enable a profile that mounts agents to receive alarms in a DSH session");
|
||||
async function disposeSticky(handle) {
|
||||
if (!handle)
|
||||
return;
|
||||
}
|
||||
if (sticky?.agent && typeof sticky.agent.followup === "function") {
|
||||
try {
|
||||
await followup(ctx, sticky.agent, prompt);
|
||||
try {
|
||||
await handle.dispose?.();
|
||||
} catch {}
|
||||
}
|
||||
async function deliverAlarmToSession(ctx, payload, lang = "zh") {
|
||||
const run = async () => {
|
||||
const prompt = formatAlarmPrompt(payload, lang);
|
||||
const agents = ctx.agents;
|
||||
if (!agents || typeof agents.create !== "function") {
|
||||
ctx.logger.warn("netxops alarm-push: ctx.agents unavailable — enable a profile that mounts agents to receive alarms in a DSH session");
|
||||
return;
|
||||
} catch (error) {
|
||||
ctx.logger.warn("netxops alarm-push: sticky followup failed, recreating session: %s", error);
|
||||
sticky = null;
|
||||
}
|
||||
}
|
||||
await createStickySession(ctx, prompt);
|
||||
if (sticky?.agent && typeof sticky.agent.followup === "function") {
|
||||
try {
|
||||
await followup(ctx, sticky.agent, prompt);
|
||||
return;
|
||||
} catch (error) {
|
||||
ctx.logger.warn("netxops alarm-push: sticky followup failed, recreating session: %s", error);
|
||||
const previous = sticky;
|
||||
sticky = null;
|
||||
await disposeSticky(previous);
|
||||
}
|
||||
}
|
||||
await createStickySession(ctx, prompt);
|
||||
};
|
||||
const next = deliveryChain.then(run, run);
|
||||
deliveryChain = next.then(() => {
|
||||
return;
|
||||
}, () => {
|
||||
return;
|
||||
});
|
||||
await next;
|
||||
}
|
||||
async function followup(ctx, agent, prompt) {
|
||||
let createUserMessage;
|
||||
|
|
@ -387,7 +420,7 @@ async function followup(ctx, agent, prompt) {
|
|||
if (typeof createUserMessage !== "function")
|
||||
throw new Error("createUserMessage missing");
|
||||
const summary = typeof mod.boundContextSummary === "function" ? mod.boundContextSummary("netx key alarm") : "netx key alarm";
|
||||
agent.followup(createUserMessage({
|
||||
await Promise.resolve(agent.followup(createUserMessage({
|
||||
content: [{ type: "text", text: prompt }],
|
||||
source: {
|
||||
kind: "webhook",
|
||||
|
|
@ -396,7 +429,7 @@ async function followup(ctx, agent, prompt) {
|
|||
form: "notice",
|
||||
summary
|
||||
}
|
||||
}));
|
||||
})));
|
||||
} catch (error) {
|
||||
const anyAgent = agent;
|
||||
if (typeof anyAgent.prompt === "function") {
|
||||
|
|
@ -455,7 +488,11 @@ async function createStickySession(ctx, prompt) {
|
|||
sessionTitle.rename(handle.agent.session, TITLE);
|
||||
}
|
||||
await followup(ctx, handle.agent, prompt);
|
||||
sticky = { sessionId, agent: handle.agent };
|
||||
sticky = {
|
||||
sessionId,
|
||||
agent: handle.agent,
|
||||
dispose: typeof handle.dispose === "function" ? () => handle.dispose() : undefined
|
||||
};
|
||||
ctx.logger.info("netxops alarm-push: opened sticky session %s", sessionId);
|
||||
} catch (error) {
|
||||
try {
|
||||
|
|
@ -465,7 +502,9 @@ async function createStickySession(ctx, prompt) {
|
|||
}
|
||||
}
|
||||
function resetAlarmSession() {
|
||||
const previous = sticky;
|
||||
sticky = null;
|
||||
disposeSticky(previous);
|
||||
}
|
||||
|
||||
// src/netx/capability-groups.ts
|
||||
|
|
@ -1619,7 +1658,9 @@ async function getSessionsExportStatus(ctx, signal) {
|
|||
}
|
||||
}
|
||||
function safePathSegment(id) {
|
||||
return id.replace(/[^A-Za-z0-9_-]/g, "_");
|
||||
const base = String(id || "").replace(/\\/g, "/").split("/").pop() ?? "";
|
||||
const cleaned = base.replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+/, "");
|
||||
return cleaned || "unnamed";
|
||||
}
|
||||
function sessionsExportZipFilename(exportedAt = new Date, host = osHostname()) {
|
||||
const stamp = [
|
||||
|
|
@ -1670,7 +1711,7 @@ async function* sessionsExportEntries(ctx, signal) {
|
|||
skipped.push({ id, reason: "no stored artifact" });
|
||||
continue;
|
||||
}
|
||||
const filename = raw.filename && raw.filename.length > 0 ? raw.filename : "session.jsonl";
|
||||
const filename = safePathSegment(raw.filename && raw.filename.length > 0 ? raw.filename : "session.jsonl") || "session.jsonl";
|
||||
const path = `sessions/${safePathSegment(id)}/${filename}`;
|
||||
artifactEntries.push({ path, content: raw.content });
|
||||
included.push({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue