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

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')
}