netxops/src/netx/group-tools-plugin.ts
oliver 3a731389f0 Add KB local write tools and align Netx Ops with standard preset.
Host-side netx__kb* tools jail create/update/delete under paths.local memories/drafts/suggestions; Netx Ops agent.cordis.yml now includes full standard capability rows plus ops tools.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 21:43:41 +08:00

171 lines
5.4 KiB
TypeScript

/**
* Shared agent-plane mount: tools + skills for one or more capability groups.
*/
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'
import {
groupsForced,
groupsForPlane,
type NetxCapabilityGroupId,
} from './capability-groups.ts'
import { registerGroupSkills } from './group-skills.ts'
import { registerKbContextSkill } from './kb-context-skill.ts'
import { registerKbLocalTools } from './kb-local-tools.ts'
import { registerKbPackSkills } from './kb-pack-skills.ts'
import { getKbContext, watchKbContext } from './kb-runtime.ts'
import { getNetxConnection, watchNetxConnection } from './runtime.ts'
import { registerNetxTools } from './tools.ts'
export interface GroupToolsPluginOptions {
/** Cordis plugin name. */
name: string
/**
* `settings` — honor inPreset/public from connection (Ops aggregate mount).
* `forced` — always register `only` groups when connection exists (per-group export).
*/
mode: 'settings' | 'forced'
/** Group allow-list; omit in settings mode to use all groups. */
only?: readonly NetxCapabilityGroupId[]
}
/**
* Apply a group-scoped tools/skills plugin on the calling (usually preset) context.
*/
export function applyGroupToolsPlugin(ctx: Context, options: GroupToolsPluginOptions): void {
let unregisterTools: (() => void) | undefined
let unregisterKbLocal: (() => void) | undefined
let unregisterSkills: (() => void) | undefined
let skillGeneration = 0
const resolveGroups = (): NetxCapabilityGroupId[] => {
const connection = getNetxConnection()
if (options.mode === 'forced') {
return groupsForced(options.only ?? [])
}
return groupsForPlane(connection?.groups, 'preset', options.only)
}
const remountTools = (): void => {
unregisterTools?.()
unregisterTools = undefined
const connection = getNetxConnection()
if (connection === undefined) {
ctx.logger.warn('%s: no connection yet — waiting for host settings bridge', options.name)
return
}
const enabled = resolveGroups()
if (enabled.length === 0) {
ctx.logger.info('%s: no groups enabled', options.name)
return
}
unregisterTools = registerNetxTools(ctx, connection, {
plane: 'preset',
only: options.mode === 'forced' ? enabled : options.only,
forceGroups: options.mode === 'forced' ? enabled : undefined,
})
ctx.logger.info(
'%s: groups=[%s] → %s tokenConfigured=%s',
options.name,
enabled.join(',') || '(none)',
connection.apiUrl,
connection.token.trim().length > 0,
)
}
const remountKbLocal = (): void => {
unregisterKbLocal?.()
unregisterKbLocal = undefined
const connection = getNetxConnection()
if (connection === undefined) return
if (connection.groupKbInPreset === false) return
unregisterKbLocal = registerKbLocalTools(ctx, getKbContext())
}
remountTools()
remountKbLocal()
const stopToolWatch = watchNetxConnection(() => {
remountTools()
remountKbLocal()
})
const stopKbToolWatch = watchKbContext(() => { remountKbLocal() })
ctx.inject(['skills'], (skillsCtx) => {
let unregisterKbSkill: (() => void) | undefined
let unregisterKbPack: (() => void) | undefined
let packGeneration = 0
const remountSkills = (): void => {
const gen = ++skillGeneration
unregisterSkills?.()
unregisterSkills = undefined
if (getNetxConnection() === undefined) return
const enabled = resolveGroups()
void registerGroupSkills(skillsCtx, enabled, options.name).then((dispose) => {
if (gen !== skillGeneration) {
dispose()
return
}
unregisterSkills = dispose
}).catch((error) => {
skillsCtx.logger.warn('%s: skill register failed: %s', options.name, error)
})
}
const remountKbSkill = (): void => {
unregisterKbSkill?.()
unregisterKbSkill = undefined
unregisterKbSkill = registerKbContextSkill(skillsCtx, getKbContext())
}
const remountKbPack = (): void => {
const gen = ++packGeneration
unregisterKbPack?.()
unregisterKbPack = undefined
const connection = getNetxConnection()
const enabled = connection?.groupKbInPreset !== false
void registerKbPackSkills(skillsCtx, getKbContext(), {
enabled,
providerLabel: `${options.name}-kb-pack`,
}).then((dispose) => {
if (gen !== packGeneration) {
dispose()
return
}
unregisterKbPack = dispose
}).catch((error) => {
skillsCtx.logger.warn('%s: kb pack skill register failed: %s', options.name, error)
})
}
remountSkills()
remountKbSkill()
remountKbPack()
const stopSkillWatch = watchNetxConnection(() => {
remountSkills()
remountKbPack()
})
const stopKbWatch = watchKbContext(() => {
remountKbSkill()
remountKbPack()
})
skillsCtx.effect(() => () => {
skillGeneration += 1
packGeneration += 1
stopSkillWatch()
stopKbWatch()
unregisterSkills?.()
unregisterSkills = undefined
unregisterKbSkill?.()
unregisterKbSkill = undefined
unregisterKbPack?.()
unregisterKbPack = undefined
}, `${options.name}: dispose skills`)
})
ctx.effect(() => () => {
stopToolWatch()
stopKbToolWatch()
unregisterTools?.()
unregisterTools = undefined
unregisterKbLocal?.()
unregisterKbLocal = undefined
}, `${options.name}: dispose tools`)
}