Move netx URL/token into DSH host settings and credentials.

Mount MCP from dsh-netxops instead of OS env in the agent preset.
This commit is contained in:
hansjone 2026-09-04 22:01:04 +08:00
parent 6677e36424
commit 45922ffd29
11 changed files with 374 additions and 117 deletions

View file

@ -1,55 +1,46 @@
# netxops — Netx Ops for DeepSeek Harness
Public **DeepSeek Harness agent preset** for network operations against [netx](https://github.com/hansjone/netx) (ZTE UME alarms, NE inventory, managed read-only CLI).
Public **DeepSeek Harness** package: host bridge (settings + credentials → netx MCP) and an **ops agent preset** (persona + playbooks).
- GitHub topic: [`dsh-plugin`](https://github.com/topics/dsh-plugin)
- npm package name: `dsh-netxops`
- Brand: **Netx Ops** (not oclaw)
- GitHub: https://github.com/hansjone/netxops
- Topic: [`dsh-plugin`](https://github.com/topics/dsh-plugin)
- npm name: `dsh-netxops`
- Brand: **Netx Ops**
## What you get (v1)
## What you get
| Piece | Location |
|-------|----------|
| Agent preset | [`presets/netxops/`](presets/netxops/) |
| Persona | `PERSONA.md` + `agent.cordis.yml` |
| Skills | `ops-netx-ume-playbook`, `ops-netx-managed-ne-playbook` |
| Tools | `@deepseek-ai/dsh-mcp-client` → `python -m netx_mcp` (`mcp__netx__*`) |
| Piece | Where | Purpose |
|-------|--------|---------|
| Host plugin `dsh-netxops` | `src/index.ts` + `cordis.patch.yml` | `apiUrl` / `lang` in settings; token as credential `NETX_API_TOKEN`; mounts `mcp__netx__*` |
| Agent preset | `presets/netxops/` | Persona + UME / managed-NE skills |
**Not in v1:** topology canvas MCP, Excel one-shot report plugin, oclaw channels/Admin.
**Not OS env:** do not put `NETX_API_URL` / `NETX_API_TOKEN` in system environment for day-to-day use — configure inside DSH (settings + credentials). See [docs/INSTALL.md](docs/INSTALL.md).
## Quick install
**Not in v1:** topology canvas MCP, Excel report plugin, Plugins settings **card** UI (Host namespace is ready; browser card follows DSH cookbook).
## Quick start
```powershell
git clone https://github.com/hansjone/netxops.git
cd netxops
# 1) Host bridge + MCP tools
dsh plugin --profile web add github:hansjone/netxops
# 2) Token (credentials store, same family as model keys)
powershell -File .\scripts\set-netx-token.ps1 -Token "nxt_…"
# 3) Ops preset (persona + skills)
powershell -File .\scripts\link-preset.ps1
```
Set `NETX_API_URL` if needed, ensure `pip install` of `netx-mcp`, then in DeepSeek Harness pick preset **Netx Ops**.
Then open DSH → session preset **Netx Ops**.
Full steps: [docs/INSTALL.md](docs/INSTALL.md) · Tool list: [docs/TOOL_MAP.md](docs/TOOL_MAP.md)
## Local debug
## Local debug (DeepSeekHarness checkout)
```powershell
powershell -File .\scripts\link-preset.ps1
# or:
# cd D:\project\DeepSeekHarness
# pnpm dsh web --patch D:\project\chatgpt\netxops\examples\local-debug\patch.cordis.yml
```
See [examples/local-debug/README.md](examples/local-debug/README.md).
## Smoke checks
1. Tools visible: `mcp__netx__aggregateUmeAlarms`, `mcp__netx__queryUmeAlarms`, …
2. “Critical Top by host” → aggregate + Result/Evidence reply shell
3. “Alarms on `<host_name>`” → host-scoped query only
Against [`DeepSeekHarness`](https://github.com/deepseek-ai/deepseek-harness) checkout: [examples/local-debug/README.md](examples/local-debug/README.md).
## Related
- Data plane: [hansjone/netx](https://github.com/hansjone/netx)
- Porting from oclaw ops: [docs/PORTING.md](docs/PORTING.md)
- Porting from oclaw: [docs/PORTING.md](docs/PORTING.md)
## License

View file

@ -1,9 +1,15 @@
# dsh-netxops host bundle (v1)
# Host-plane Netx Ops bridge: settings + credentials → mcp__netx__* tools.
#
# Agent composition lives in presets/netxops/ (install into ~/.dsh/.agent-presets).
# This host-plane patch is intentionally empty: MCP and persona are scoped to the
# netxops agent preset so other agents are not flooded with mcp__netx__* tools.
#
# Install: see README.md / docs/INSTALL.md
# Optional: dsh plugin add github:hansjone/netxops (registers this bundle; still link the preset)
[]
# After `dsh plugin add` / link, open Settings → Plugins to edit apiUrl / lang
# (settings card client half; see docs/INSTALL.md). Token: credential NETX_API_TOKEN.
- insert:
- id: netxops
name: dsh-netxops
config:
apiUrl: http://127.0.0.1:8890
lang: zh
pythonCommand: python
tokenCredentialRef: NETX_API_TOKEN
toolCallTimeoutMs: 120000
failOnStartupError: false

View file

@ -2,7 +2,7 @@
## Prerequisites
1. **DeepSeek Harness** (`dsh`) installed or a local checkout (see [local debug](../examples/local-debug/README.md)).
1. **DeepSeek Harness** (`dsh`) installed or a local checkout.
2. **netx API** reachable (default `http://127.0.0.1:8890`).
3. **Python 3.11+** on the same machine as `dsh`, with `netx_mcp` importable:
@ -11,48 +11,73 @@ pip install "git+https://github.com/hansjone/netx.git#subdirectory=packages/netx
python -c "import netx_mcp; print('ok')"
```
4. Environment (optional if defaults match):
You do **not** need to export `NETX_API_URL` / `NETX_API_TOKEN` in the OS environment for normal use.
| Variable | Default | Purpose |
|----------|---------|---------|
| `NETX_API_URL` | `http://127.0.0.1:8890` | netx REST root |
| `NETX_API_TOKEN` | (empty / auto file) | Bearer token |
| `NETX_LANG` | `zh` | API locale |
## Install the agent preset (recommended)
Clone this repo, then link or copy the preset directory into the DSH user preset root:
```powershell
git clone https://github.com/hansjone/netxops.git
# Windows (developer junction — editable in place):
powershell -File .\scripts\link-preset.ps1
# Or copy:
# Copy-Item -Recurse .\presets\netxops $env:USERPROFILE\.dsh\.agent-presets\netxops
```
Unix:
```bash
git clone https://github.com/hansjone/netxops.git
./scripts/link-preset.sh
# Or: cp -R presets/netxops ~/.dsh/.agent-presets/netxops
```
Restart / open `dsh web`, create a session, choose preset **Netx Ops** (`netxops`).
## Optional: `dsh plugin add`
## 1) Install host bridge (URL + token + MCP tools)
```bash
dsh plugin --profile <name> add github:hansjone/netxops
# or from a checkout:
dsh plugin --profile <name> add /path/to/netxops
```
v1’s host `cordis.patch.yml` is empty (MCP stays inside the preset). You still need the preset link/copy above.
This applies `cordis.patch.yml`, which mounts package `dsh-netxops`. That plugin:
- Registers settings namespace **`netxops`** (`apiUrl`, `lang`, `pythonCommand`, …)
- Reads bearer token from credentials reference **`NETX_API_TOKEN`** (same store as model keys: `~/.dsh/.credentials.yaml`)
- Spawns `python -m netx_mcp` and registers `mcp__netx__*` on the **host** tool registry
### Fill API URL / language
**Target UX:** Settings → **Plugins** → **Netx Ops** card (settings `apiUrl` / `lang`).
The Host settings namespace is already registered; a dedicated Plugins settings card (browser half) follows the [DSH settings-card cookbook](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-settings-card.md) and will land in a follow-up.
**Until the card ships**, change composition defaults in the profile patch / plugin config:
```yaml
# ~/.dsh/profiles/<name>/… or override the dsh-netxops row
config:
apiUrl: http://10.0.0.5:8890
lang: zh
pythonCommand: python
```
Or edit the installed bundle row after `dsh plugin add`.
### Fill netx API token (secret)
Same credential store as DeepSeek API keys — **not** a process env var:
```powershell
powershell -File .\scripts\set-netx-token.ps1 -Token "nxt_…"
```
```bash
./scripts/set-netx-token.sh 'nxt_…'
```
This writes / updates `refs.NETX_API_TOKEN` under `%USERPROFILE%\.dsh\.credentials.yaml` (or `$DSH_HOME`). Restart is not required if DSH is watching credentials; otherwise restart `dsh web`.
## 2) Install the agent preset (persona + skills)
```powershell
git clone https://github.com/hansjone/netxops.git
cd netxops
powershell -File .\scripts\link-preset.ps1
```
Unix: `./scripts/link-preset.sh`
Open `dsh web`, new session → preset **Netx Ops**.
## Verify
1. Session tools include `mcp__netx__queryUmeAlarms` (and siblings).
2. Ask: “Critical Top 10 by host” → expect `aggregateUmeAlarms` + Result/Evidence shell.
3. Ask a single `host_name` alarm query → host-scoped tools only.
1. Tools include `mcp__netx__queryUmeAlarms` (and siblings).
2. “Critical Top by host” → aggregate + Result/Evidence shell.
3. Single `host_name` alarm query → host-scoped tools only.
See [TOOL_MAP.md](TOOL_MAP.md).
## Local debug (DeepSeekHarness checkout)
See [examples/local-debug/README.md](../examples/local-debug/README.md).

View file

@ -4,12 +4,14 @@
| Source (oclaw) | Destination |
|----------------|-------------|
| `runtime/workspaces/ops/ROLE_SYSTEM.md` | `presets/netxops/PERSONA.md` + persona text in `agent.cordis.yml` (brand **Netx Ops**) |
| `runtime/workspaces/ops/ROLE_SYSTEM.md` | `presets/netxops/PERSONA.md` + persona in `agent.cordis.yml` (brand **Netx Ops**) |
| `skills/_workspace/ops/ops-netx-ume-playbook/` | `presets/netxops/skills/ops-netx-ume-playbook/` |
| `skills/_workspace/ops/ops-netx-managed-ne-playbook/` | `presets/netxops/skills/ops-netx-managed-ne-playbook/` |
| netx MCP contract | `agent.cordis.yml` → `dsh-mcp-client` |
| netx MCP + API URL/token | Host plugin `src/index.ts` (`dsh-netxops`): settings namespace `netxops` + credential `NETX_API_TOKEN` → dynamic `dsh-mcp-client` |
Adaptations: removed oclaw Admin / WhatsApp / `ume_alarm_xlsx_report` / wiki capture / skill_auto_install lanes; fiber/offline recipes use Raw/aggregate MCP only.
Adaptations: removed oclaw Admin / WhatsApp / `ume_alarm_xlsx_report` / wiki capture / skill_auto_install; fiber/offline recipes use Raw/aggregate MCP only.
**Config UX:** not OS env. Token via DSH credentials (same as model keys); `apiUrl` via settings / composition config. Plugins settings **card** (browser half) TBD per DSH cookbook.
## Stay in oclaw
@ -17,6 +19,7 @@ Gateway, WhatsApp/Weixin, Admin MCP UI, specialist router, memory-wiki, ops-ai H
## Phase 2 candidates
- Plugins settings card (`dsh.client`) for apiUrl + token on Settings → Plugins
- `ume_alarm_xlsx_report` + thin HTTP client as a DSH tool plugin
- UME sync context inject
- `ops-ip-knowledge-playbook` + KB content

View file

@ -1,31 +1,33 @@
# Local debug against DeepSeekHarness
## Link the preset
From this repo root:
## A. Link preset + install host bridge
```powershell
cd D:\project\chatgpt\netxops
powershell -File .\scripts\link-preset.ps1
powershell -File .\scripts\set-netx-token.ps1 -Token (Get-Content D:\project\chatgpt\netx\data\auth\mcp_token -Raw).Trim()
```
This junctions `presets\netxops` → `%USERPROFILE%\.dsh\.agent-presets\netxops` so DSH’s default `includeUserRoot` discovers it.
## Optional patch (extra preset root)
If you prefer not to touch `~/.dsh/.agent-presets`, start DSH with:
From DeepSeekHarness (source):
```powershell
cd D:\project\DeepSeekHarness
pnpm dsh plugin --profile web add D:\project\chatgpt\netxops
# or one-shot overlay:
pnpm dsh web --patch D:\project\chatgpt\netxops\examples\local-debug\patch.cordis.yml
```
`patch.cordis.yml` replaces the `agent-presets` row to keep shipped + user roots and add this repo’s `presets/` directory. Adjust the path if your checkout differs.
## B. What the debug patch does
## Env
[`patch.cordis.yml`](patch.cordis.yml):
```powershell
$env:NETX_API_URL = "http://127.0.0.1:8890"
# $env:NETX_API_TOKEN = "nxt_..."
```
1. Ensures agent-presets can see `D:/project/chatgpt/netxops/presets`
2. Inserts host plugin `dsh-netxops` via **absolute path** to `src/index.ts` (no npm link required)
Ensure `python -m netx_mcp` works in the same environment PATH that DSH will spawn.
Adjust paths if your checkout differs.
## C. Verify
1. New session → preset **Netx Ops**
2. Tools include `mcp__netx__aggregateUmeAlarms`
3. Ask Critical Top / single host alarms

View file

@ -1,10 +1,9 @@
# Local DeepSeekHarness overlay: discover netxops preset from this checkout.
# Local DeepSeekHarness overlay: discover netxops preset + mount host bridge.
#
# Usage (from DeepSeekHarness root):
# pnpm dsh web --patch D:/project/chatgpt/netxops/examples/local-debug/patch.cordis.yml
#
# Prefer scripts/link-preset.ps1 when possible — it uses the default user preset root
# and does not replace host agent-presets config.
# Prefer also: scripts/link-preset.ps1 and scripts/set-netx-token.ps1
- replace:
- id: agent-presets
@ -16,3 +15,14 @@
roots:
- path: D:/project/chatgpt/netxops/presets
trust: user
- insert:
- id: netxops
name: D:/project/chatgpt/netxops/src/index.ts
config:
apiUrl: http://127.0.0.1:8890
lang: zh
pythonCommand: python
tokenCredentialRef: NETX_API_TOKEN
toolCallTimeoutMs: 120000
failOnStartupError: false

View file

@ -1,11 +1,17 @@
{
"name": "dsh-netxops",
"version": "0.1.0",
"description": "DeepSeek Harness agent preset for Netx Ops (UME alarms, NE inventory, managed CLI)",
"version": "0.1.1",
"description": "DeepSeek Harness Netx Ops: host bridge (settings/credentials → netx MCP) + agent preset",
"license": "MIT",
"type": "module",
"private": false,
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./package.json": "./package.json"
},
"files": [
"src/",
"presets/",
"cordis.patch.yml",
"docs/",
@ -36,7 +42,12 @@
}
},
"peerDependencies": {
"@deepseek-ai/cordis": "*",
"@deepseek-ai/schemastery": "*",
"@deepseek-ai/dsh-credentials": "*",
"@deepseek-ai/dsh-settings": "*",
"@deepseek-ai/dsh-mcp-client": "*",
"@deepseek-ai/dsh-tools": "*",
"@deepseek-ai/dsh-persona": "*",
"@deepseek-ai/dsh-skill-filesystem": "*",
"@deepseek-ai/dsh-tool-skill": "*",

View file

@ -63,20 +63,6 @@
- id: tool-skill
name: '@deepseek-ai/dsh-tool-skill'
# ── netx MCP (UME + managed CLI; no topology canvas in v1) ──────────────────
- id: mcp-netx
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: netx
transport: stdio
command: python
args:
- -m
- netx_mcp
env:
NETX_API_URL: !!js process.env.NETX_API_URL || 'http://127.0.0.1:8890'
NETX_API_TOKEN: !!js process.env.NETX_API_TOKEN || ''
NETX_LANG: !!js process.env.NETX_LANG || 'zh'
toolCallTimeoutMs: 120000
failOnStartupError: false
# 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.

View file

@ -0,0 +1,38 @@
# Set NETX_API_TOKEN in the DSH credentials store (no OS env required).
param(
[Parameter(Mandatory = $true)]
[string] $Token,
[string] $DshHome = $(if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE ".dsh" })
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($Token)) { throw "Token is empty" }
New-Item -ItemType Directory -Force -Path $DshHome | Out-Null
$path = Join-Path $DshHome ".credentials.yaml"
# Minimal merge: preserve other refs if file exists and is simple version:1 docs.
$escaped = $Token.Replace("'", "''")
if (-not (Test-Path $path)) {
@"
version: 1
refs:
NETX_API_TOKEN: '$escaped'
"@ | Set-Content -Path $path -Encoding utf8
Write-Host "Created $path with NETX_API_TOKEN"
exit 0
}
$raw = Get-Content -Path $path -Raw
if ($raw -match '(?m)^\s*NETX_API_TOKEN\s*:') {
$raw = [regex]::Replace($raw, '(?m)^(\s*NETX_API_TOKEN\s*:\s*).*$', "`${1}'$escaped'")
} elseif ($raw -match '(?m)^refs\s*:') {
$raw = [regex]::Replace($raw, '(?m)^(refs\s*:\s*\r?\n)', "`${1} NETX_API_TOKEN: '$escaped'`n")
} else {
$raw = $raw.TrimEnd() + "`n`nrefs:`n NETX_API_TOKEN: '$escaped'`n"
}
Set-Content -Path $path -Value $raw -Encoding utf8
Write-Host "Updated NETX_API_TOKEN in $path"
Write-Host "If dsh is running, wait for credential reload or restart dsh web."

43
scripts/set-netx-token.sh Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
TOKEN="${1:-}"
if [[ -z "$TOKEN" ]]; then
echo "usage: $0 <NETX_API_TOKEN>" >&2
exit 1
fi
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
mkdir -p "$DSH_HOME"
PATH_FILE="$DSH_HOME/.credentials.yaml"
ESC=${TOKEN//\'/\'\'}
if [[ ! -f "$PATH_FILE" ]]; then
cat >"$PATH_FILE" <<EOF
version: 1
refs:
NETX_API_TOKEN: '$ESC'
EOF
echo "Created $PATH_FILE with NETX_API_TOKEN"
exit 0
fi
if grep -qE '^[[:space:]]*NETX_API_TOKEN[[:space:]]*:' "$PATH_FILE"; then
# portable-ish in-place replace
tmp="$(mktemp)"
sed -E "s|^([[:space:]]*NETX_API_TOKEN[[:space:]]*:[[:space:]]*).*$|\1'$ESC'|" "$PATH_FILE" >"$tmp"
mv "$tmp" "$PATH_FILE"
else
if grep -qE '^refs[[:space:]]*:' "$PATH_FILE"; then
tmp="$(mktemp)"
awk -v tok="$ESC" '
BEGIN{done=0}
/^refs[[:space:]]*:/ && !done { print; print " NETX_API_TOKEN: '\''" tok "'\''"; done=1; next }
{ print }
' "$PATH_FILE" >"$tmp"
mv "$tmp" "$PATH_FILE"
else
printf '\nrefs:\n NETX_API_TOKEN: '\''%s'\''\n' "$ESC" >>"$PATH_FILE"
fi
fi
echo "Updated NETX_API_TOKEN in $PATH_FILE"
echo "If dsh is running, wait for credential reload or restart dsh web."

142
src/index.ts Normal file
View file

@ -0,0 +1,142 @@
/**
* 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`.
*
* @module dsh-netxops
*/
import type { Context, Fiber } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import type {} from '@deepseek-ai/dsh-credentials'
import type {} from '@deepseek-ai/dsh-settings'
import * as McpClient from '@deepseek-ai/dsh-mcp-client'
/** Cordis plugin name. */
export const name = 'netxops'
/** MCP tools registry must exist to mount the child client. */
export const inject = ['tools']
/** Settings / composition namespace (Plugins page join key). */
export const NETXOPS_SETTINGS_NAMESPACE = 'netxops'
/** Default credential reference for the netx API bearer token. */
export const DEFAULT_TOKEN_REF = 'NETX_API_TOKEN'
/** Plugin / settings section shape. */
export interface Config {
/** netx REST root (no trailing slash required). */
apiUrl: string
/** Passed to MCP as `NETX_LANG`. */
lang: string
/** Executable that can run `python -m netx_mcp`. */
pythonCommand: string
/** Credential reference for the bearer token (never store the secret here). */
tokenCredentialRef: string
/** Per MCP tool-call timeout (ms). */
toolCallTimeoutMs: number
/** Fail activation when MCP cannot connect / sync tools. */
failOnStartupError: boolean
}
export const Config: z<Config> = z.object({
apiUrl: z.string().default('http://127.0.0.1:8890'),
lang: z.string().default('zh'),
pythonCommand: z.string().default('python'),
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),
})
/**
* Resolve bearer token from the credentials seam (or empty when unset).
*/
async function resolveToken(ctx: Context, refName: string): Promise<string> {
const credentials = ctx.get('credentials')
if (credentials === undefined) return ''
const hit = await credentials.resolve(credentialRef(refName))
return hit?.value ?? ''
}
/**
* Apply the Netx Ops host bridge.
*/
export function apply(ctx: Context, config: Config = Config({})): void {
let source: () => Config = () => config
let mcpFiber: Fiber | undefined
let remounting: Promise<void> = Promise.resolve()
let generation = 0
const remount = (): void => {
remounting = remounting.then(async () => {
const gen = ++generation
const previous = mcpFiber
mcpFiber = undefined
if (previous !== undefined) {
try {
await previous.dispose()
} catch (error) {
ctx.logger.warn('netxops: disposing previous mcp-client failed: %s', error)
}
}
if (gen !== generation) return
const current = source()
const token = await resolveToken(ctx, current.tokenCredentialRef)
if (gen !== generation) return
const mcpConfig = McpClient.Config({
transport: 'stdio',
serverName: 'netx',
command: current.pythonCommand,
args: ['-m', 'netx_mcp'],
env: {
NETX_API_URL: current.apiUrl.replace(/\/$/, ''),
NETX_API_TOKEN: token,
NETX_LANG: current.lang,
},
toolCallTimeoutMs: current.toolCallTimeoutMs,
failOnStartupError: current.failOnStartupError,
})
try {
mcpFiber = await ctx.plugin(McpClient, mcpConfig)
} catch (error) {
ctx.logger.error('netxops: failed to mount mcp-client: %s', error)
if (current.failOnStartupError) throw error
}
}).catch((error) => {
ctx.logger.error('netxops: remount error: %s', error)
})
}
// Composition defaults first (works even when settings provider is absent).
remount()
ctx.inject(['settings'], (settingsCtx) => {
settingsCtx.settings.installSection(ctx, NETXOPS_SETTINGS_NAMESPACE, Config, config, {
setSource: (current) => {
source = current
},
onChange: () => {
remount()
},
})
})
ctx.on('credentials/reference-updated', (ref) => {
if (String(ref) === source().tokenCredentialRef) remount()
})
ctx.effect(() => () => {
generation += 1
const fiber = mcpFiber
mcpFiber = undefined
void fiber?.dispose()
}, 'netxops: dispose mcp-client')
}