Register KB pack skills from operator-subset _skills (P2).

Load hasSkills packs onto preset/public planes with groupKb toggles; unify content has* keys and keep netx-ops pure.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-13 11:46:31 +08:00
parent fcdd11ac59
commit 2caa3c98d7
26 changed files with 1122 additions and 106 deletions

View file

@ -4,7 +4,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
// src/index.ts
import { cpSync, existsSync as existsSync3, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { homedir as homedir3 } from "node:os";
import { dirname as dirname3, join as join4 } from "node:path";
import { dirname as dirname3, join as join5 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url";
import z from "@deepseek-ai/schemastery";
import { credentialRef } from "@deepseek-ai/dsh-credentials";
@ -807,7 +807,11 @@ function skillBody(snapshot) {
`| kbVersion | ${snapshot.version} |`,
`| kbContent (on) | ${flags} |`,
"",
"Environment mirrors: `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT`, `KB_STATUS`."
"Environment mirrors: KB_ROOT, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_STATUS.",
"",
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
`${snapshot.realRoot}/_skills/ when hasSkills is true — use those skills for KB triage;`,
"keep netx-ops for live netx evidence only."
].join(`
`)
};
@ -818,7 +822,7 @@ function skillBody(snapshot) {
content: [
"## Knowledge base (unavailable)",
"",
`kbStatus=\`${snapshot.status}\`. ${reason}`,
`kbStatus=${snapshot.status}. ${reason}`,
"",
"**Do not invent an operator, country, or KB paths.**",
"Use only netx tools / live evidence (alarms, inventory, CLI, topology).",
@ -846,10 +850,18 @@ function registerKbContextSkill(ctx, snapshot) {
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
var EMPTY_CONTENT = {
regions: false,
theory: false,
packet: false,
skills: false
hasRegions: false,
hasTheory: false,
hasPacket: false,
hasCommon: false,
hasSkills: false
};
var LEGACY_CONTENT_KEY = {
regions: "hasRegions",
theory: "hasTheory",
packet: "hasPacket",
skills: "hasSkills",
common: "hasCommon"
};
function unconfiguredKbSnapshot() {
return {
@ -962,8 +974,17 @@ function parseContent(raw) {
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;
const row = raw;
const present = new Set(Object.keys(row));
for (const [key, value] of Object.entries(row)) {
const on = value === true;
const mapped = LEGACY_CONTENT_KEY[key];
if (mapped !== undefined) {
if (on && !present.has(mapped))
out[mapped] = true;
continue;
}
out[key] = on;
}
return out;
}
@ -1047,6 +1068,70 @@ function resolveKbRoot(kbRoot, maxDepth = 3) {
}
}
// src/netx/kb-pack-skills.ts
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
import { join as join4 } from "node:path";
function kbPackSkillsEnabled(snapshot) {
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
}
async function registerKbPackSkills(ctx, snapshot, options) {
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
return () => {};
}
const skillsApi = ctx.skills;
if (!skillsApi || typeof skillsApi.register !== "function") {
return () => {};
}
const root = join4(snapshot.realRoot, "_skills");
let entries;
try {
entries = await readdir2(root);
} catch {
ctx.logger.info("netxops: kb pack skills dir missing at %s — skip", root);
return () => {};
}
const disposers = [];
const provider = options.providerLabel ?? "netxops-kb-pack";
for (const entry of entries) {
const full = join4(root, entry);
let isDir = false;
try {
isDir = (await stat2(full)).isDirectory();
} catch {
continue;
}
if (!isDir)
continue;
let skill;
try {
skill = await loadSkillBundle(full);
} catch (error) {
ctx.logger.warn("netxops: kb pack skill load failed at %s: %s", full, error instanceof Error ? error.message : String(error));
continue;
}
if (!skill) {
ctx.logger.warn("netxops: kb pack skill skipped (invalid SKILL.md): %s", full);
continue;
}
disposers.push(skillsApi.register({
name: skill.name,
description: skill.description,
content: skill.content,
path: skill.path,
resourceBase: { kind: "directory", path: skill.directory },
provider,
source: "custom"
}));
}
if (disposers.length > 0) {
ctx.logger.info("netxops: kb pack skills registered count=%s root=%s provider=%s", disposers.length, root, provider);
}
return () => {
for (const dispose of disposers)
dispose();
};
}
// src/netx/kb-runtime.ts
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
var ENV_KEYS = [
@ -3709,33 +3794,35 @@ var Config = z.object({
groupOpsPublic: z.boolean().default(false),
groupTopologyInPreset: z.boolean().default(false),
groupTopologyPublic: z.boolean().default(false),
kbRoot: z.string().default("")
kbRoot: z.string().default(""),
groupKbInPreset: z.boolean().default(true),
groupKbPublic: z.boolean().default(false)
});
function packageRoot() {
return join4(dirname3(fileURLToPath2(import.meta.url)), "..");
return join5(dirname3(fileURLToPath2(import.meta.url)), "..");
}
function resolveDshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
if (fromEnv !== undefined && fromEnv.length > 0)
return fromEnv;
return join4(homedir3(), ".dsh");
return join5(homedir3(), ".dsh");
}
function ensureAgentPresetInstalled(logger) {
const src = join4(packageRoot(), "presets", NETXOPS_PRESET_ID);
const composition = join4(src, "agent.cordis.yml");
const src = join5(packageRoot(), "presets", NETXOPS_PRESET_ID);
const composition = join5(src, "agent.cordis.yml");
if (!existsSync3(composition)) {
logger.warn("netxops: bundled preset missing at %s — skip user-preset install", src);
return;
}
const destParent = join4(resolveDshHome(), ".agent-presets");
const dest = join4(destParent, NETXOPS_PRESET_ID);
const destParent = join5(resolveDshHome(), ".agent-presets");
const dest = join5(destParent, NETXOPS_PRESET_ID);
try {
mkdirSync(destParent, { recursive: true });
if (existsSync3(dest)) {
rmSync(dest, { recursive: true, force: true });
}
cpSync(src, dest, { recursive: true });
writeFileSync(join4(dest, ".dsh-netxops-managed"), `${new Date().toISOString()}
writeFileSync(join5(dest, ".dsh-netxops-managed"), `${new Date().toISOString()}
`, "utf8");
logger.info("netxops: agent preset installed at %s", dest);
} catch (error) {
@ -3825,7 +3912,9 @@ function apply(ctx, config = Config({})) {
token,
lang: current.lang,
toolCallTimeoutMs: current.toolCallTimeoutMs,
groups
groups,
groupKbInPreset: current.groupKbInPreset !== false,
groupKbPublic: current.groupKbPublic === true
});
const kb = resolveKbRoot(current.kbRoot ?? "");
publishKbContext(kb);
@ -3887,7 +3976,9 @@ function apply(ctx, config = Config({})) {
ctx.inject(["skills"], (skillsCtx) => {
let unregisterSkills;
let unregisterKbSkill;
let unregisterKbPack;
let generation2 = 0;
let packGeneration = 0;
const remountPublicSkills = () => {
const gen = ++generation2;
unregisterSkills?.();
@ -3914,20 +4005,44 @@ function apply(ctx, config = Config({})) {
unregisterKbSkill = registerKbContextSkill(skillsCtx, snapshot);
skillsCtx.logger.info("netxops: kb-context skill status=%s", snapshot.status);
};
const remountKbPack = () => {
const gen = ++packGeneration;
unregisterKbPack?.();
unregisterKbPack = undefined;
const connection = getNetxConnection();
const enabled = connection?.groupKbPublic === true;
registerKbPackSkills(skillsCtx, getKbContext(), {
enabled,
providerLabel: "netxops-kb-pack-public"
}).then((dispose) => {
if (gen !== packGeneration) {
dispose();
return;
}
unregisterKbPack = dispose;
}).catch((error) => {
skillsCtx.logger.warn("netxops: public kb pack skill register failed: %s", error);
});
};
remountPublicSkills();
remountKbSkill();
remountKbPack();
const stopWatch = watchNetxConnection(() => {
remountPublicSkills();
remountKbPack();
});
const stopKbWatch = watchKbContext(() => {
remountKbSkill();
remountKbPack();
});
skillsCtx.effect(() => () => {
generation2 += 1;
packGeneration += 1;
stopWatch();
stopKbWatch();
unregisterSkills?.();
unregisterKbSkill?.();
unregisterKbPack?.();
}, "netxops: dispose public skills");
});
ctx.inject(["connection"], (connCtx) => {