Netx tools: load from netx remote /mcp, add per-tool toggles

- Drop the TypeScript netx tool implementations and the hard-coded
  TOOLS_BY_GROUP list; register netx__* tools from the server's
  tools/list (group from _meta.netx.group) and forward tools/call.
- Thin Streamable HTTP client on standard fetch (fetchImpl injectable);
  cached catalog when netx is unreachable, refresh to reconnect.
- disabledTools (deny list; new server tools follow their group) and
  readOnlyOnly (only readOnlyHint=true tools), re-registered live.
- Settings card: per-group expandable tool rows with switches and a
  write marker; skills append a disabled-tools note.
- Toggles only control model visibility; netx enforces security.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-16 22:08:22 +08:00
parent 2def53e7c7
commit 2ef3a43c63
38 changed files with 5049 additions and 17029 deletions

View file

@ -11,6 +11,7 @@ import { registerGroupSkills } from '../src/netx/group-skills.ts'
import { publishNetxConnection } from '../src/netx/runtime.ts'
import { getToolMountStatuses, publishToolMountStatus, resetToolMountStatus } from '../src/netx/tool-mount-status.ts'
import { asToolsMountView } from '../src/client/tools-status-view.ts'
import { OPS_TOOL_NAMES, clearMcpCatalogSeed, seedMcpCatalog } from './mcp-test-catalog.mjs'
const log = { info() {}, warn() {}, error() {} }
const pause = () => new Promise(resolve => setTimeout(resolve, 40))
@ -37,7 +38,12 @@ function fakeContext(failAt = -1) {
function diagnosticHome(t) {
const previous = process.env.DSH_HOME
process.env.DSH_HOME = mkdtempSync(join(tmpdir(), 'netxops-publication-'))
t.after(() => { if (previous === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = previous })
seedMcpCatalog()
t.after(() => {
clearMcpCatalogSeed()
if (previous === undefined) delete process.env.DSH_HOME
else process.env.DSH_HOME = previous
})
}
test('selected presets are isolated and global publication is inherited once', () => {
@ -67,7 +73,7 @@ test('failed tool registration rolls back every earlier tool', t => {
const { ctx, tools } = fakeContext(3)
assert.throws(() => registerNetxTools(ctx, connection(), { plane: 'preset' }), /registration refused/)
assert.equal(tools.size, 0)
assert.ok(getToolMountStatuses().some(row => row.error === 'registration refused'))
assert.ok(getToolMountStatuses().some(row => String(row.error ?? '').includes('registration refused')))
resetToolMountStatus()
})
@ -87,7 +93,7 @@ test('turning off a mounted group revokes tools, KB context and pending skill lo
publishNetxConnection(connection({ groupBizMonitorInPreset: false }))
const mount = fakeContext()
applyGroupToolsPlugin(mount.ctx, { name: 'lifecycle', mode: 'settings' })
assert.equal(mount.tools.size, 15)
assert.equal(mount.tools.size, OPS_TOOL_NAMES.length)
assert.equal(mount.skills.has('kb-context'), true)
publishNetxConnection(connection({ toolsEnabled: false }))
await pause()
@ -95,7 +101,7 @@ test('turning off a mounted group revokes tools, KB context and pending skill lo
assert.equal(mount.skills.size, 0)
publishNetxConnection(connection({ groupBizMonitorInPreset: false }))
await pause()
assert.equal(mount.tools.size, 15)
assert.equal(mount.tools.size, OPS_TOOL_NAMES.length)
assert.equal(mount.skills.has('netx-ops'), true)
mount.dispose()
await pause()
@ -123,9 +129,9 @@ test('mount inventory survives cross-bundle publishes and reports all scopes', a
entry.apply(mount.ctx)
const statuses = getToolMountStatuses()
assert.ok(statuses.some(row => row.id === 'host-test'))
assert.ok(statuses.some(row => row.count === 15))
assert.ok(statuses.some(row => row.count === OPS_TOOL_NAMES.length))
const view = asToolsMountView({ mounts: statuses, connection: { toolsEnabled: true } })
assert.equal(view.count, 15)
assert.equal(view.count, OPS_TOOL_NAMES.length)
mount.dispose()
assert.equal(getToolMountStatuses().some(row => row.count > 0), false)
resetToolMountStatus()