Auto-install the Netx Ops agent preset on plugin activate.

Copy presets into ~/.dsh/.agent-presets (not a Windows junction — DSH discovery skips those) so dsh plugin add alone surfaces Custom → Netx Ops with skills.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-04 23:09:58 +08:00
parent 4854101afd
commit 5179eb74b1
9 changed files with 174 additions and 82 deletions

View file

@ -1,38 +1,33 @@
# netxops — Netx Ops for DeepSeek Harness
Public **DeepSeek Harness** package: host bridge + **Plugins settings card** + ops agent preset.
Public **DeepSeek Harness** package: host bridge + Plugins card + **agent preset (skills included)**.
- GitHub: https://github.com/hansjone/netxops
- Topic: [`dsh-plugin`](https://github.com/topics/dsh-plugin)
- Package: `dsh-netxops`
- Brand: **Netx Ops**
## What you get
| Piece | Where | Purpose |
|-------|--------|---------|
| Host plugin | `src/index.ts` + `cordis.patch.yml` | settings `netxops` + credential `NETX_API_TOKEN` → `mcp__netx__*` |
| Plugins card | `src/client/` → `lib/client.js` | Settings → Plugins → fill URL / token in UI |
| Agent preset | `presets/netxops/` | Persona + UME / managed-NE skills |
**No OS env required** for day-to-day use. See [docs/INSTALL.md](docs/INSTALL.md).
**Not in v1:** topology canvas MCP, Excel report plugin.
## Quick start
## Install (one command)
```powershell
dsh plugin --profile web add github:hansjone/netxops
dsh web
# Settings → Plugins → Netx Ops → token + API URL → Save
```
Agent preset (skills): clone once and run `scripts/link-preset.ps1` (or `.sh`).
Then:
## Related
1. Settings → **Plugins** → **Netx Ops** → API URL / token
2. Settings → **Agent presets** → Custom → **Netx Ops** (installed automatically on first host activate)
3. New session → choose **Netx Ops**
- Data plane: [hansjone/netx](https://github.com/hansjone/netx)
- Porting: [docs/PORTING.md](docs/PORTING.md)
Also need: running **netx API**, and `pip install` of `netx_mcp` (Python execution plane). Details: [docs/INSTALL.md](docs/INSTALL.md).
## What is coupled
| In the plugin | Outside (data / runtime) |
|---------------|---------------------------|
| MCP spawn + settings + credentials | netx HTTP API |
| Persona + UME / managed-NE skills | `python -m netx_mcp` on PATH |
| Agent preset auto-install to `~/.dsh/.agent-presets` | |
## License

View file

@ -12,4 +12,4 @@
pythonCommand: python
tokenCredentialRef: NETX_API_TOKEN
toolCallTimeoutMs: 120000
failOnStartupError: false
installAgentPreset: true

View file

@ -1,52 +1,52 @@
# Install Netx Ops on DeepSeek Harness
## Prerequisites
## What `dsh plugin add` gives you
1. **DeepSeek Harness** (`dsh`) installed.
2. **netx API** reachable (default `http://127.0.0.1:8890`).
3. **Python 3.11+** with `netx_mcp` importable:
One install wires **all of**:
| Piece | How you use it |
|-------|----------------|
| Host bridge | spawns `mcp__netx__*` via `python -m netx_mcp` |
| Plugins card | Settings → Plugins → **Netx Ops** (URL / lang / python) |
| Agent preset + skills | Settings → Agent presets → **Custom → Netx Ops** (copied into `~/.dsh/.agent-presets` on first boot) |
You do **not** run `link-preset.ps1` for normal use. That script is only a manual fallback.
## Still required outside the npm package
1. **netx API** reachable (default `http://127.0.0.1:8890`) — the data plane.
2. **Python `netx_mcp`** on the same machine as `dsh`:
```powershell
pip install "git+https://github.com/hansjone/netx.git#subdirectory=packages/netx-mcp"
python -c "import netx_mcp; print('ok')"
```
No OS env vars needed for URL / token.
v1 keeps the Python MCP as the execution plane (shared with OpenClaw / `python -m netx_mcp`). The DSH plugin owns orchestration, settings, persona, and skills — not a second MCP install in the agent preset.
## Install (GitHub — recommended)
## Install
```powershell
dsh plugin --profile web add github:hansjone/netxops
dsh web
# or from DeepSeekHarness source:
# pnpm dsh plugin --profile web add github:hansjone/netxops
dsh web # or: pnpm dsh web
```
Then: **Settings → Plugins → Netx Ops** → paste API token + URL → Save.
That is the whole host install. You do **not** run any `link-dsh-peers` script for GitHub installs.
### Agent preset (persona + skills)
One-time (or after pull):
```powershell
git clone https://github.com/hansjone/netxops.git
cd netxops
powershell -File .\scripts\link-preset.ps1
```
Unix: `./scripts/link-preset.sh`
New session → preset **Netx Ops**.
1. **Settings → Plugins → Netx Ops** → API URL (+ token if the field is enabled).
Token fallback: `scripts/set-netx-token.ps1` / `.sh`.
2. Restart or open Settings → **Agent presets** → Custom → **Netx Ops** should appear after the host plugin has activated once.
3. **New session → preset Netx Ops** → ask e.g. Critical Top / single-host alarms.
## Verify
1. Settings → Plugins shows **Netx Ops**; token badge “Configured” after save.
2. Tools include `mcp__netx__queryUmeAlarms`.
3. Critical Top / single-host alarm queries work.
1. Plugins card **Netx Ops** visible.
2. Agent presets → Custom → **Netx Ops**.
3. Tools include `mcp__netx__queryUmeAlarms`.
See [TOOL_MAP.md](TOOL_MAP.md).
## Path / local checkout (developers only)
## Path / local checkout (developers)
```powershell
dsh plugin --profile web add D:\path\to\netxops

View file

@ -1,4 +1,8 @@
// src/index.ts
import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import z from "@deepseek-ai/schemastery";
import { credentialRef } from "@deepseek-ai/dsh-credentials";
import * as DshSettings from "@deepseek-ai/dsh-settings";
@ -6,6 +10,7 @@ import * as McpClient from "@deepseek-ai/dsh-mcp-client";
var name = "netxops";
var inject = ["tools"];
var NETXOPS_SETTINGS_NAMESPACE = "netxops";
var NETXOPS_PRESET_ID = "netxops";
var DEFAULT_TOKEN_REF = "NETX_API_TOKEN";
var Config2 = z.object({
apiUrl: z.string().default("http://127.0.0.1:8890"),
@ -13,8 +18,40 @@ var Config2 = z.object({
pythonCommand: z.string().default("python"),
tokenCredentialRef: z.string().role("credential-ref").default(DEFAULT_TOKEN_REF),
toolCallTimeoutMs: z.number().step(1).min(1000).default(120000),
failOnStartupError: z.boolean().default(false)
failOnStartupError: z.boolean().default(false),
installAgentPreset: z.boolean().default(true)
});
function packageRoot() {
return join(dirname(fileURLToPath(import.meta.url)), "..");
}
function resolveDshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
if (fromEnv !== undefined && fromEnv.length > 0)
return fromEnv;
return join(homedir(), ".dsh");
}
function ensureAgentPresetInstalled(logger) {
const src = join(packageRoot(), "presets", NETXOPS_PRESET_ID);
const composition = join(src, "agent.cordis.yml");
if (!existsSync(composition)) {
logger.warn("netxops: bundled preset missing at %s — skip user-preset install", src);
return;
}
const destParent = join(resolveDshHome(), ".agent-presets");
const dest = join(destParent, NETXOPS_PRESET_ID);
try {
mkdirSync(destParent, { recursive: true });
if (existsSync(dest)) {
rmSync(dest, { recursive: true, force: true });
}
cpSync(src, dest, { recursive: true });
writeFileSync(join(dest, ".dsh-netxops-managed"), `${new Date().toISOString()}
`, "utf8");
logger.info("netxops: agent preset installed at %s", dest);
} catch (error) {
logger.error("netxops: failed to install agent preset: %s", error);
}
}
async function resolveToken(ctx, refName) {
const credentials = ctx.get("credentials");
if (credentials === undefined)
@ -37,6 +74,9 @@ function apply(ctx, config = Config2({})) {
let mcpFiber;
let remounting = Promise.resolve();
let generation = 0;
if (config.installAgentPreset) {
ensureAgentPresetInstalled(ctx.logger);
}
const remount = () => {
remounting = remounting.then(async () => {
const gen = ++generation;
@ -102,8 +142,10 @@ function apply(ctx, config = Config2({})) {
export {
name,
inject,
ensureAgentPresetInstalled,
apply,
NETXOPS_SETTINGS_NAMESPACE,
NETXOPS_PRESET_ID,
DEFAULT_TOKEN_REF,
Config2 as Config
};

View file

@ -1,6 +1,6 @@
{
"name": "dsh-netxops",
"version": "0.1.10",
"version": "0.1.11",
"description": "DeepSeek Harness Netx Ops: host bridge + Plugins settings card + agent preset",
"license": "MIT",
"type": "module",

View file

@ -63,6 +63,7 @@
- id: tool-skill
name: '@deepseek-ai/dsh-tool-skill'
# netx MCP is mounted on the HOST by package `dsh-netxops` (cordis.patch.yml):
# Settings → Plugins / credentials NETX_API_TOKEN — not OS env, not this preset.
# Tools appear as mcp__netx__* for sessions that can see the host tool registry.
# netx MCP is mounted on the HOST by package `dsh-netxops` (cordis.patch.yml).
# The same package copies this preset into `$DSH_HOME/.agent-presets/netxops`
# on activate — no manual link-preset script for normal installs.
# Tools appear as mcp__netx__* on the host tool registry.

View file

@ -1,4 +1,6 @@
# Link presets/netxops into the DSH user agent-presets root (Windows junction).
# Link / install presets/netxops into the DSH user agent-presets root.
# Prefer a real directory copy: DSH discovery skips Windows junctions
# (Dirent.isDirectory() is false for reparse points).
$ErrorActionPreference = "Stop"
$RepoRoot = Split-Path -Parent $PSScriptRoot
$Source = Join-Path $RepoRoot "presets\netxops"
@ -6,18 +8,14 @@ $DshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFIL
$TargetParent = Join-Path $DshHome ".agent-presets"
$Target = Join-Path $TargetParent "netxops"
if (-not (Test-Path $Source)) {
if (-not (Test-Path (Join-Path $Source "agent.cordis.yml"))) {
throw "Missing preset dir: $Source"
}
New-Item -ItemType Directory -Force -Path $TargetParent | Out-Null
if (Test-Path $Target) {
$item = Get-Item $Target -Force
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
cmd /c "rmdir `"$Target`""
} else {
throw "Target exists and is not a junction: $Target — remove or rename it first."
}
Remove-Item -Recurse -Force $Target
}
cmd /c "mklink /J `"$Target`" `"$Source`""
Write-Host "Linked $Target -> $Source"
Write-Host "Restart dsh / open a new session and select preset 'netxops' (Netx Ops)."
Copy-Item -Recurse -Force $Source $Target
Set-Content -Path (Join-Path $Target ".dsh-netxops-managed") -Value ((Get-Date).ToString("o"))
Write-Host "Installed $Target (copy from $Source)"
Write-Host "Open Settings → Agent presets → Custom → Netx Ops (or new session)."

View file

@ -1,19 +1,19 @@
#!/usr/bin/env bash
# Install presets/netxops into $DSH_HOME/.agent-presets (copy, not symlink).
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SOURCE="$REPO_ROOT/presets/netxops"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SRC="$ROOT/presets/netxops"
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
TARGET_PARENT="$DSH_HOME/.agent-presets"
TARGET="$TARGET_PARENT/netxops"
DEST_PARENT="$DSH_HOME/.agent-presets"
DEST="$DEST_PARENT/netxops"
if [[ ! -d "$SOURCE" ]]; then
echo "Missing preset dir: $SOURCE" >&2
if [[ ! -f "$SRC/agent.cordis.yml" ]]; then
echo "Missing preset dir: $SRC" >&2
exit 1
fi
mkdir -p "$TARGET_PARENT"
if [[ -e "$TARGET" || -L "$TARGET" ]]; then
rm -rf "$TARGET"
fi
ln -s "$SOURCE" "$TARGET"
echo "Linked $TARGET -> $SOURCE"
echo "Restart dsh / open a new session and select preset 'netxops' (Netx Ops)."
mkdir -p "$DEST_PARENT"
rm -rf "$DEST"
cp -R "$SRC" "$DEST"
date -u +%Y-%m-%dT%H:%M:%SZ > "$DEST/.dsh-netxops-managed"
echo "Installed $DEST"
echo "Open Settings → Agent presets → Custom → Netx Ops"

View file

@ -2,13 +2,17 @@
* Host-plane Netx Ops bridge: settings (apiUrl / lang / python) + credentials
* (NETX_API_TOKEN) drive a dynamically mounted `@deepseek-ai/dsh-mcp-client`.
*
* Configure in DSH Settings → Plugins → Netx Ops (settings card when client
* half is installed). Token is stored as credential `NETX_API_TOKEN` (same
* store as model API keys). Helper: `scripts/set-netx-token.ps1`.
* On activate, the agent preset + skills are copied into
* `$DSH_HOME/.agent-presets/netxops` so `dsh plugin add` alone is enough
* (Windows junctions are invisible to DSH's `Dirent.isDirectory()` scan).
*
* @module dsh-netxops
*/
import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Context, Fiber } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
@ -26,6 +30,9 @@ export const inject = ['tools']
/** Settings / composition namespace (Plugins page join key). */
export const NETXOPS_SETTINGS_NAMESPACE = 'netxops'
/** Agent preset id under `$DSH_HOME/.agent-presets/`. */
export const NETXOPS_PRESET_ID = 'netxops'
/** Default credential reference for the netx API bearer token. */
export const DEFAULT_TOKEN_REF = 'NETX_API_TOKEN'
@ -43,6 +50,11 @@ export interface Config {
toolCallTimeoutMs: number
/** Fail activation when MCP cannot connect / sync tools. */
failOnStartupError: boolean
/**
* Copy bundled agent preset + skills into `$DSH_HOME/.agent-presets/netxops`
* on every activate (required for Settings → Agent presets).
*/
installAgentPreset: boolean
}
export const Config: z<Config> = z.object({
@ -52,8 +64,49 @@ export const Config: z<Config> = z.object({
tokenCredentialRef: z.string().role('credential-ref').default(DEFAULT_TOKEN_REF),
toolCallTimeoutMs: z.number().step(1).min(1000).default(120_000),
failOnStartupError: z.boolean().default(false),
installAgentPreset: z.boolean().default(true),
})
/** Package root (parent of `lib/` or `src/` depending on launch). */
function packageRoot(): string {
return join(dirname(fileURLToPath(import.meta.url)), '..')
}
/** Harness home used by agent-presets' user root. */
function resolveDshHome(): string {
const fromEnv = process.env.DSH_HOME?.trim()
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv
return join(homedir(), '.dsh')
}
/**
* Install the bundled Netx Ops preset as a real directory under the user
* preset root. DSH discovery skips Windows junctions (`Dirent.isDirectory()`
* is false for reparse points), so a copy is required — not `mklink /J`.
* @param logger - cordis logger for non-fatal install failures.
*/
export function ensureAgentPresetInstalled(logger: Context['logger']): void {
const src = join(packageRoot(), 'presets', NETXOPS_PRESET_ID)
const composition = join(src, 'agent.cordis.yml')
if (!existsSync(composition)) {
logger.warn('netxops: bundled preset missing at %s — skip user-preset install', src)
return
}
const destParent = join(resolveDshHome(), '.agent-presets')
const dest = join(destParent, NETXOPS_PRESET_ID)
try {
mkdirSync(destParent, { recursive: true })
if (existsSync(dest)) {
rmSync(dest, { recursive: true, force: true })
}
cpSync(src, dest, { recursive: true })
writeFileSync(join(dest, '.dsh-netxops-managed'), `${new Date().toISOString()}\n`, 'utf8')
logger.info('netxops: agent preset installed at %s', dest)
} catch (error) {
logger.error('netxops: failed to install agent preset: %s', error)
}
}
/**
* Resolve bearer token from the credentials seam (or empty when unset).
*/
@ -105,6 +158,10 @@ export function apply(ctx: Context, config: Config = Config({})): void {
let remounting: Promise<void> = Promise.resolve()
let generation = 0
if (config.installAgentPreset) {
ensureAgentPresetInstalled(ctx.logger)
}
const remount = (): void => {
remounting = remounting.then(async () => {
const gen = ++generation
@ -148,7 +205,6 @@ export function apply(ctx: Context, config: Config = Config({})): void {
})
}
// Composition defaults first (works even when settings provider is absent).
remount()
installNetxopsSettings(ctx, config, {