/** * Host-plane Netx Ops: settings (apiUrl / lang) + credentials (NETX_API_TOKEN) * publish a connection snapshot; the Ops preset mounts `netx__*` into its own * tool scope (`dsh-netxops/tools`) so other agents do not see them. * * 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 } 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 * as DshSettings from '@deepseek-ai/dsh-settings' import type {} from '@deepseek-ai/dsh-settings' import { publishNetxConnection } from './netx/runtime.ts' /** Cordis plugin name. */ export const name = 'netxops' /** 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' /** Plugin / settings section shape. */ export interface Config { /** netx REST root (no trailing slash required). */ apiUrl: string /** Passed as `lang` query when starting with `en`. */ lang: string /** Credential reference for the bearer token (never store the secret here). */ tokenCredentialRef: string /** Per tool-call timeout (ms). */ toolCallTimeoutMs: number /** * 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 = z.object({ apiUrl: z.string().default('http://127.0.0.1:8890'), lang: z.string().default('zh'), tokenCredentialRef: z.string().role('credential-ref').default(DEFAULT_TOKEN_REF), toolCallTimeoutMs: z.number().step(1).min(1000).default(120_000), 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). */ async function resolveToken(ctx: Context, refName: string): Promise { const credentials = ctx.get('credentials') if (credentials === undefined) return '' const hit = await credentials.resolve(credentialRef(refName)) return hit?.value ?? '' } /** * Register the settings namespace on both dsh generations: * - 0.1.1-rc.2: standalone `installSettingsSection` * - ≥0.1.2-rc.1: `settings.installSection` on the provider */ function installNetxopsSettings( ctx: Context, entry: Config, hooks: { setSource: (current: () => Config) => void onChange: () => void }, ): void { const legacy = (DshSettings as { installSettingsSection?: ( context: Context, ns: string, schema: z, base: Config, sectionHooks: typeof hooks, ) => void }).installSettingsSection if (typeof legacy === 'function') { legacy(ctx, NETXOPS_SETTINGS_NAMESPACE, Config, entry, hooks) return } ctx.inject(['settings'], (settingsCtx) => { settingsCtx.settings.installSection(ctx, NETXOPS_SETTINGS_NAMESPACE, Config, entry, hooks) }) } /** * Apply the Netx Ops host bridge (settings + connection publish; tools live on the preset). */ export function apply(ctx: Context, config: Config = Config({})): void { let source: () => Config = () => config let publishing: Promise = Promise.resolve() let generation = 0 if (config.installAgentPreset) { ensureAgentPresetInstalled(ctx.logger) } const publish = (): void => { publishing = publishing.then(async () => { const gen = ++generation const current = source() const token = await resolveToken(ctx, current.tokenCredentialRef) if (gen !== generation) return publishNetxConnection({ apiUrl: current.apiUrl.replace(/\/$/, ''), token, lang: current.lang, toolCallTimeoutMs: current.toolCallTimeoutMs, }) ctx.logger.info('netxops: published connection → %s', current.apiUrl.replace(/\/$/, '')) }).catch((error) => { ctx.logger.error('netxops: connection publish error: %s', error) }) } publish() installNetxopsSettings(ctx, config, { setSource: (current) => { source = current }, onChange: () => { publish() }, }) ctx.on('credentials/reference-updated', (ref) => { if (String(ref) === source().tokenCredentialRef) publish() }) ctx.effect(() => () => { generation += 1 }, 'netxops: dispose host bridge') }