Add operator-subset knowledge base wiring for Netx Ops P1.

Locate and validate MANIFEST under kbRoot, show anti-mixup status in settings, and inject KB_* env plus kb-context so sessions degrade to pure netx when unset.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-13 10:39:28 +08:00
parent e6f9d062ed
commit de7c6a0c4b
23 changed files with 2694 additions and 68 deletions

View file

@ -2,9 +2,9 @@ import { createRequire } from "node:module";
var __require = /* @__PURE__ */ createRequire(import.meta.url);
// src/index.ts
import { cpSync, existsSync as existsSync2, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { cpSync, existsSync as existsSync3, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { homedir as homedir3 } from "node:os";
import { dirname as dirname2, join as join3 } from "node:path";
import { dirname as dirname3, join as join4 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url";
import z from "@deepseek-ai/schemastery";
import { credentialRef } from "@deepseek-ai/dsh-credentials";
@ -785,28 +785,331 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
};
}
// src/netx/runtime.ts
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
// src/netx/kb-context-skill.ts
var SKILL_NAME = "kb-context";
function skillBody(snapshot) {
if (snapshot.status === "configured") {
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
return {
description: "Operator knowledge-base context for this Host (paths + identity from MANIFEST).",
content: [
"## Knowledge base (configured)",
"",
"When troubleshooting with operator playbooks or docs, **compose paths from this root**.",
"Do not invent another operator or country.",
"",
`| Field | Value |`,
`| --- | --- |`,
`| kbStatus | configured |`,
`| kbRoot | \`${snapshot.realRoot}\` |`,
`| kbOperator | ${snapshot.operatorName} |`,
`| kbCountry | ${snapshot.country} |`,
`| kbVersion | ${snapshot.version} |`,
`| kbContent (on) | ${flags} |`,
"",
"Environment mirrors: `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT`, `KB_STATUS`."
].join(`
`)
};
}
const reason = snapshot.status === "error" ? snapshot.errorMessage || "invalid knowledge base" : "kbRoot is empty";
return {
description: "Knowledge-base context is unavailable — stay on pure netx evidence.",
content: [
"## Knowledge base (unavailable)",
"",
`kbStatus=\`${snapshot.status}\`. ${reason}`,
"",
"**Do not invent an operator, country, or KB paths.**",
"Use only netx tools / live evidence (alarms, inventory, CLI, topology).",
"Operator-specific playbooks are out of scope until a valid MANIFEST is configured."
].join(`
`)
};
}
function registerKbContextSkill(ctx, snapshot) {
const skillsApi = ctx.skills;
if (!skillsApi || typeof skillsApi.register !== "function") {
return () => {};
}
const body = skillBody(snapshot);
return skillsApi.register({
name: SKILL_NAME,
description: body.description,
content: body.content,
provider: "netxops-kb",
source: "custom"
});
}
// src/netx/kb-manifest.ts
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
import { dirname as dirname2, join as join3, resolve } from "node:path";
var EMPTY_CONTENT = {
regions: false,
theory: false,
packet: false,
skills: false
};
function unconfiguredKbSnapshot() {
return {
status: "unconfigured",
realRoot: "",
operatorName: "",
country: "",
version: "",
content: { ...EMPTY_CONTENT },
errorMessage: ""
};
}
function errorSnapshot(message) {
return {
status: "error",
realRoot: "",
operatorName: "",
country: "",
version: "",
content: { ...EMPTY_CONTENT },
errorMessage: message
};
}
function findManifest(kbRoot, maxDepth = 3) {
const root = resolve(kbRoot.trim());
if (!kbRoot.trim()) {
return { paths: [], error: "kbRoot is empty" };
}
let rootStat;
try {
rootStat = statSync(root);
} catch {
return { paths: [], error: `kbRoot not found: ${root}` };
}
if (!rootStat.isDirectory()) {
return { paths: [], error: `kbRoot is not a directory: ${root}` };
}
const direct = join3(root, "MANIFEST.json");
if (existsSync2(direct)) {
try {
if (statSync(direct).isFile())
return { paths: [direct] };
} catch {}
}
const found = [];
const walk = (dir, depth) => {
if (depth > maxDepth)
return;
const candidate = join3(dir, "MANIFEST.json");
if (existsSync2(candidate)) {
try {
if (statSync(candidate).isFile())
found.push(candidate);
} catch {}
}
if (depth === maxDepth)
return;
let entries;
try {
entries = readdirSync(dir);
} catch {
return;
}
for (const name of entries) {
if (name === "node_modules" || name === ".git")
continue;
const full = join3(dir, name);
let st;
try {
st = statSync(full);
} catch {
continue;
}
if (st.isDirectory())
walk(full, depth + 1);
}
};
let topEntries;
try {
topEntries = readdirSync(root);
} catch {
return { paths: [], error: `cannot read kbRoot: ${root}` };
}
for (const name of topEntries) {
if (name === "node_modules" || name === ".git")
continue;
const full = join3(root, name);
let st;
try {
st = statSync(full);
} catch {
continue;
}
if (st.isDirectory())
walk(full, 1);
}
return { paths: found };
}
function asNonEmptyString(value, field) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(`MANIFEST missing required string field: ${field}`);
}
return value.trim();
}
function parseContent(raw) {
const out = { ...EMPTY_CONTENT };
if (raw === undefined || raw === null) {
throw new Error("MANIFEST missing required object field: content");
}
if (typeof raw !== "object" || Array.isArray(raw)) {
throw new Error("MANIFEST content must be an object");
}
for (const [key, value] of Object.entries(raw)) {
out[key] = value === true;
}
return out;
}
function parseManifest(raw) {
let data;
try {
data = JSON.parse(raw);
} catch (error) {
throw new Error(`MANIFEST is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
}
if (data === null || typeof data !== "object" || Array.isArray(data)) {
throw new Error("MANIFEST root must be an object");
}
const row = data;
if (row.schemaVersion !== "1.0") {
throw new Error(`unsupported schemaVersion (want "1.0", got ${JSON.stringify(row.schemaVersion)})`);
}
if (row.packageType !== "operator-subset") {
throw new Error(`unsupported packageType (want "operator-subset", got ${JSON.stringify(row.packageType)})`);
}
const operator = row.operator;
if (operator === null || typeof operator !== "object" || Array.isArray(operator)) {
throw new Error("MANIFEST missing required object field: operator");
}
const op = operator;
return {
operatorName: asNonEmptyString(op.name, "operator.name"),
country: asNonEmptyString(op.country, "operator.country"),
version: asNonEmptyString(row.version, "version"),
content: parseContent(row.content)
};
}
function resolveKbRoot(kbRoot, maxDepth = 3) {
const trimmed = kbRoot.trim();
if (!trimmed)
return unconfiguredKbSnapshot();
const located = findManifest(trimmed, maxDepth);
if (located.error)
return errorSnapshot(located.error);
if (located.paths.length === 0) {
return errorSnapshot(`no MANIFEST.json under ${resolve(trimmed)} (maxDepth=${maxDepth})`);
}
if (located.paths.length > 1) {
return errorSnapshot(`ambiguous MANIFEST.json (${located.paths.length} hits); pick a unique package root`);
}
const manifestPath = located.paths[0];
let raw;
try {
raw = readFileSync(manifestPath, "utf8");
} catch (error) {
return errorSnapshot(`cannot read MANIFEST: ${error instanceof Error ? error.message : String(error)}`);
}
try {
const parsed = parseManifest(raw);
return {
status: "configured",
realRoot: dirname2(manifestPath),
operatorName: parsed.operatorName,
country: parsed.country,
version: parsed.version,
content: parsed.content,
errorMessage: ""
};
} catch (error) {
return errorSnapshot(error instanceof Error ? error.message : String(error));
}
}
// src/netx/kb-runtime.ts
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
var ENV_KEYS = [
"KB_ROOT",
"KB_OPERATOR",
"KB_COUNTRY",
"KB_VERSION",
"KB_CONTENT",
"KB_STATUS"
];
function store2() {
const root = globalThis;
let current = root[STORE_KEY2];
if (current === undefined) {
current = { connection: undefined, listeners: new Set };
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
root[STORE_KEY2] = current;
}
return current;
}
function publishNetxConnection(next) {
function getKbContext() {
return { ...store2().snapshot, content: { ...store2().snapshot.content } };
}
function publishKbContext(next) {
const state = store2();
state.snapshot = {
...next,
content: { ...next.content }
};
for (const listener of state.listeners)
listener();
}
function watchKbContext(listener) {
const state = store2();
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
};
}
function applyKbEnv(snapshot) {
for (const key of ENV_KEYS) {
delete process.env[key];
}
process.env.KB_STATUS = snapshot.status;
if (snapshot.status !== "configured")
return;
process.env.KB_ROOT = snapshot.realRoot;
process.env.KB_OPERATOR = snapshot.operatorName;
process.env.KB_COUNTRY = snapshot.country;
process.env.KB_VERSION = snapshot.version;
process.env.KB_CONTENT = JSON.stringify(snapshot.content);
}
function resetKbContext() {
publishKbContext(unconfiguredKbSnapshot());
applyKbEnv(unconfiguredKbSnapshot());
}
// src/netx/runtime.ts
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
function store3() {
const root = globalThis;
let current = root[STORE_KEY3];
if (current === undefined) {
current = { connection: undefined, listeners: new Set };
root[STORE_KEY3] = current;
}
return current;
}
function publishNetxConnection(next) {
const state = store3();
state.connection = next;
for (const listener of state.listeners)
listener();
}
function getNetxConnection() {
return store2().connection;
return store3().connection;
}
function watchNetxConnection(listener) {
const state = store2();
const state = store3();
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
@ -3390,33 +3693,34 @@ var Config = z.object({
groupOpsInPreset: z.boolean().default(true),
groupOpsPublic: z.boolean().default(false),
groupTopologyInPreset: z.boolean().default(false),
groupTopologyPublic: z.boolean().default(false)
groupTopologyPublic: z.boolean().default(false),
kbRoot: z.string().default("")
});
function packageRoot() {
return join3(dirname2(fileURLToPath2(import.meta.url)), "..");
return join4(dirname3(fileURLToPath2(import.meta.url)), "..");
}
function resolveDshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
if (fromEnv !== undefined && fromEnv.length > 0)
return fromEnv;
return join3(homedir3(), ".dsh");
return join4(homedir3(), ".dsh");
}
function ensureAgentPresetInstalled(logger) {
const src = join3(packageRoot(), "presets", NETXOPS_PRESET_ID);
const composition = join3(src, "agent.cordis.yml");
if (!existsSync2(composition)) {
const src = join4(packageRoot(), "presets", NETXOPS_PRESET_ID);
const composition = join4(src, "agent.cordis.yml");
if (!existsSync3(composition)) {
logger.warn("netxops: bundled preset missing at %s — skip user-preset install", src);
return;
}
const destParent = join3(resolveDshHome(), ".agent-presets");
const dest = join3(destParent, NETXOPS_PRESET_ID);
const destParent = join4(resolveDshHome(), ".agent-presets");
const dest = join4(destParent, NETXOPS_PRESET_ID);
try {
mkdirSync(destParent, { recursive: true });
if (existsSync2(dest)) {
if (existsSync3(dest)) {
rmSync(dest, { recursive: true, force: true });
}
cpSync(src, dest, { recursive: true });
writeFileSync(join3(dest, ".dsh-netxops-managed"), `${new Date().toISOString()}
writeFileSync(join4(dest, ".dsh-netxops-managed"), `${new Date().toISOString()}
`, "utf8");
logger.info("netxops: agent preset installed at %s", dest);
} catch (error) {
@ -3508,6 +3812,14 @@ function apply(ctx, config = Config({})) {
toolCallTimeoutMs: current.toolCallTimeoutMs,
groups
});
const kb = resolveKbRoot(current.kbRoot ?? "");
publishKbContext(kb);
applyKbEnv(kb);
if (kb.status === "configured") {
ctx.logger.info("netxops: knowledge base configured → %s (%s / %s v%s)", kb.realRoot, kb.operatorName, kb.country, kb.version);
} else if (kb.status === "error") {
ctx.logger.warn("netxops: knowledge base error — %s", kb.errorMessage);
}
restartAlarmPush(current, apiUrl, token);
if (!tokenConfigured) {
ctx.logger.warn("netxops: published connection → %s tokenConfigured=false (set credential %s)", apiUrl, current.tokenCredentialRef);
@ -3559,6 +3871,7 @@ function apply(ctx, config = Config({})) {
});
ctx.inject(["skills"], (skillsCtx) => {
let unregisterSkills;
let unregisterKbSkill;
let generation2 = 0;
const remountPublicSkills = () => {
const gen = ++generation2;
@ -3579,14 +3892,27 @@ function apply(ctx, config = Config({})) {
skillsCtx.logger.warn("netxops: public skill register failed: %s", error);
});
};
const remountKbSkill = () => {
unregisterKbSkill?.();
unregisterKbSkill = undefined;
const snapshot = getKbContext();
unregisterKbSkill = registerKbContextSkill(skillsCtx, snapshot);
skillsCtx.logger.info("netxops: kb-context skill status=%s", snapshot.status);
};
remountPublicSkills();
remountKbSkill();
const stopWatch = watchNetxConnection(() => {
remountPublicSkills();
});
const stopKbWatch = watchKbContext(() => {
remountKbSkill();
});
skillsCtx.effect(() => () => {
generation2 += 1;
stopWatch();
stopKbWatch();
unregisterSkills?.();
unregisterKbSkill?.();
}, "netxops: dispose public skills");
});
ctx.inject(["connection"], (connCtx) => {
@ -3596,10 +3922,17 @@ function apply(ctx, config = Config({})) {
connCtx.logger.warn("netxops: connection.rpc.handle unavailable — alarm status UI disabled");
} else {
connCtx.effect(() => {
const dispose = rpc.handle(NETXOPS_RPC_CHANNEL, async (endpoint) => {
const dispose = rpc.handle(NETXOPS_RPC_CHANNEL, async (endpoint, payload) => {
if (endpoint === "alarm-push.status") {
return { ok: true, value: getAlarmPushStatus() };
}
if (endpoint === "kb.status") {
return { ok: true, value: getKbContext() };
}
if (endpoint === "kb.resolve") {
const path = extractKbResolvePath(payload);
return { ok: true, value: resolveKbRoot(path) };
}
if (endpoint === "sessions.export.status") {
const value = await getSessionsExportStatus(ctx);
return { ok: true, value };
@ -3672,8 +4005,23 @@ function apply(ctx, config = Config({})) {
stopAlarmPush = undefined;
resetAlarmSession();
resetAlarmPushStatus();
resetKbContext();
}, "netxops: dispose host bridge");
}
function extractKbResolvePath(payload) {
if (payload === null || typeof payload !== "object" || Array.isArray(payload))
return "";
const row = payload;
if (typeof row.path === "string")
return row.path;
const args = row.args;
if (args !== null && typeof args === "object" && !Array.isArray(args)) {
const path = args.path;
if (typeof path === "string")
return path;
}
return "";
}
export {
name,
inject,