mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 04:00:46 +08:00
Scope netx__* tools to the Netx Ops preset only.
Register tools from the preset standing mount so standard and other agents no longer see them without Ops skills. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ed219842d3
commit
0f30d92302
11 changed files with 180 additions and 697 deletions
45
src/agent-tools.ts
Normal file
45
src/agent-tools.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Agent-plane Netx Ops tools: register `netx__*` into the calling context's
|
||||
* tool scope (the Netx Ops preset standing mount), so other presets do not see them.
|
||||
*
|
||||
* @module dsh-netxops/tools
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import { getNetxConnection, watchNetxConnection } from './netx/runtime.ts'
|
||||
import { registerNetxTools } from './netx/tools.ts'
|
||||
|
||||
/** Cordis plugin name. */
|
||||
export const name = 'netxops-tools'
|
||||
|
||||
/** Tool registry must exist in this (preset-scoped) context. */
|
||||
export const inject = ['tools']
|
||||
|
||||
/**
|
||||
* Mount netx REST tools for the Netx Ops agent preset only.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
let unregister: (() => void) | undefined
|
||||
|
||||
const remount = (): void => {
|
||||
unregister?.()
|
||||
unregister = undefined
|
||||
const connection = getNetxConnection()
|
||||
if (connection === undefined) {
|
||||
ctx.logger.warn('netxops-tools: no connection yet — waiting for host settings bridge')
|
||||
return
|
||||
}
|
||||
unregister = registerNetxTools(ctx, connection)
|
||||
ctx.logger.info('netxops-tools: registered netx__* for Ops preset → %s', connection.apiUrl)
|
||||
}
|
||||
|
||||
remount()
|
||||
const stopWatch = watchNetxConnection(() => { remount() })
|
||||
|
||||
ctx.effect(() => () => {
|
||||
stopWatch()
|
||||
unregister?.()
|
||||
unregister = undefined
|
||||
}, 'netxops-tools: dispose')
|
||||
}
|
||||
38
src/index.ts
38
src/index.ts
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* Host-plane Netx Ops: settings (apiUrl / lang) + credentials (NETX_API_TOKEN)
|
||||
* drive native `netx__*` tools that call the netx REST API directly.
|
||||
* 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
|
||||
|
|
@ -19,15 +20,11 @@ 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 type {} from '@deepseek-ai/dsh-tools'
|
||||
import { registerNetxTools } from './netx/tools.ts'
|
||||
import { publishNetxConnection } from './netx/runtime.ts'
|
||||
|
||||
/** Cordis plugin name. */
|
||||
export const name = 'netxops'
|
||||
|
||||
/** Tool registry must exist to register `netx__*` tools. */
|
||||
export const inject = ['tools']
|
||||
|
||||
/** Settings / composition namespace (Plugins page join key). */
|
||||
export const NETXOPS_SETTINGS_NAMESPACE = 'netxops'
|
||||
|
||||
|
|
@ -145,59 +142,52 @@ function installNetxopsSettings(
|
|||
}
|
||||
|
||||
/**
|
||||
* Apply the Netx Ops host bridge.
|
||||
* 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 unregister: (() => void) | undefined
|
||||
let remounting: Promise<void> = Promise.resolve()
|
||||
let publishing: Promise<void> = Promise.resolve()
|
||||
let generation = 0
|
||||
|
||||
if (config.installAgentPreset) {
|
||||
ensureAgentPresetInstalled(ctx.logger)
|
||||
}
|
||||
|
||||
const remount = (): void => {
|
||||
remounting = remounting.then(async () => {
|
||||
const publish = (): void => {
|
||||
publishing = publishing.then(async () => {
|
||||
const gen = ++generation
|
||||
unregister?.()
|
||||
unregister = undefined
|
||||
if (gen !== generation) return
|
||||
|
||||
const current = source()
|
||||
const token = await resolveToken(ctx, current.tokenCredentialRef)
|
||||
if (gen !== generation) return
|
||||
|
||||
unregister = registerNetxTools(ctx, {
|
||||
publishNetxConnection({
|
||||
apiUrl: current.apiUrl.replace(/\/$/, ''),
|
||||
token,
|
||||
lang: current.lang,
|
||||
toolCallTimeoutMs: current.toolCallTimeoutMs,
|
||||
})
|
||||
ctx.logger.info('netxops: registered netx__* REST tools → %s', current.apiUrl.replace(/\/$/, ''))
|
||||
ctx.logger.info('netxops: published connection → %s', current.apiUrl.replace(/\/$/, ''))
|
||||
}).catch((error) => {
|
||||
ctx.logger.error('netxops: remount error: %s', error)
|
||||
ctx.logger.error('netxops: connection publish error: %s', error)
|
||||
})
|
||||
}
|
||||
|
||||
remount()
|
||||
publish()
|
||||
|
||||
installNetxopsSettings(ctx, config, {
|
||||
setSource: (current) => {
|
||||
source = current
|
||||
},
|
||||
onChange: () => {
|
||||
remount()
|
||||
publish()
|
||||
},
|
||||
})
|
||||
|
||||
ctx.on('credentials/reference-updated', (ref) => {
|
||||
if (String(ref) === source().tokenCredentialRef) remount()
|
||||
if (String(ref) === source().tokenCredentialRef) publish()
|
||||
})
|
||||
|
||||
ctx.effect(() => () => {
|
||||
generation += 1
|
||||
unregister?.()
|
||||
unregister = undefined
|
||||
}, 'netxops: dispose netx tools')
|
||||
}, 'netxops: dispose host bridge')
|
||||
}
|
||||
|
|
|
|||
54
src/netx/runtime.ts
Normal file
54
src/netx/runtime.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Process-local Netx Ops connection snapshot shared between the host settings
|
||||
* bridge and the Ops-preset-scoped tool plugin.
|
||||
*
|
||||
* Uses `Symbol.for` on `globalThis` so host + agent-tools bundles share one store
|
||||
* even when Bun emits them as separate ESM files.
|
||||
*/
|
||||
|
||||
import type { NetxToolConnection } from './tools.ts'
|
||||
|
||||
type Listener = () => void
|
||||
|
||||
interface Store {
|
||||
connection: NetxToolConnection | undefined
|
||||
listeners: Set<Listener>
|
||||
}
|
||||
|
||||
const STORE_KEY = Symbol.for('dsh-netxops.connection-store')
|
||||
|
||||
function store(): Store {
|
||||
const root = globalThis as typeof globalThis & { [STORE_KEY]?: Store }
|
||||
let current = root[STORE_KEY]
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set() }
|
||||
root[STORE_KEY] = current
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the latest API URL / token / lang / timeout for Ops tool mounts.
|
||||
* @param next - connection used by the next `registerNetxTools` call.
|
||||
*/
|
||||
export function publishNetxConnection(next: NetxToolConnection): void {
|
||||
const state = store()
|
||||
state.connection = next
|
||||
for (const listener of state.listeners) listener()
|
||||
}
|
||||
|
||||
/** @returns the last published connection, if any. */
|
||||
export function getNetxConnection(): NetxToolConnection | undefined {
|
||||
return store().connection
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to connection publishes (settings / credential remounts).
|
||||
* @param listener - called synchronously after each publish.
|
||||
* @returns disposer.
|
||||
*/
|
||||
export function watchNetxConnection(listener: Listener): () => void {
|
||||
const state = store()
|
||||
state.listeners.add(listener)
|
||||
return () => { state.listeners.delete(listener) }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue