mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-19 20:23:19 +08:00
feat(skills): merge unavailable tools into footnote, bind cache to token, prune stale caches, warn on remote plain HTTP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
056f2d4ea3
commit
05bfd97c6e
19 changed files with 1100 additions and 496 deletions
|
|
@ -54,7 +54,7 @@ test('registerGroupSkills appends disabled footnote; skips disabled groups', asy
|
|||
lang: 'zh',
|
||||
})
|
||||
assert.ok(registered.length >= 1)
|
||||
assert.match(registered[0].content, /以下工具已禁用:netx__execManagedNe,不要调用/)
|
||||
assert.match(registered[0].content, /以下工具已禁用或当前 token 无权使用:netx__execManagedNe,不要调用/)
|
||||
dispose()
|
||||
|
||||
registered.length = 0
|
||||
|
|
@ -71,6 +71,6 @@ test('registerGroupSkills appends disabled footnote; skips disabled groups', asy
|
|||
disabledByGroup: { ops: ['netx__execManagedNe'] },
|
||||
lang: 'en',
|
||||
})
|
||||
assert.match(registered[0].content, /The following tools are disabled: netx__execManagedNe/)
|
||||
assert.match(registered[0].content, /disabled or not granted to this token: netx__execManagedNe/)
|
||||
dispose3()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import http from 'node:http'
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, utimesSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ import { refreshMcpCatalog, resetMcpCatalog } from '../src/netx/mcp-store.ts'
|
|||
import { publishNetxConnection } from '../src/netx/runtime.ts'
|
||||
import {
|
||||
hasSkillsCache,
|
||||
usableSkillsCacheDir,
|
||||
listCachedSkills,
|
||||
replaceSkillsCache,
|
||||
skillsCacheDir,
|
||||
|
|
@ -48,6 +49,7 @@ function skillResources(files) {
|
|||
group: f.group,
|
||||
skill: f.skill,
|
||||
entry: f.relPath === 'SKILL.md',
|
||||
...(f.unavailableTools ? { unavailableTools: f.unavailableTools } : {}),
|
||||
},
|
||||
},
|
||||
text: f.text,
|
||||
|
|
@ -365,3 +367,55 @@ test('listCachedSkills summarizes cached skills per group for the Plugins card',
|
|||
])
|
||||
assert.deepEqual(listCachedSkills('').skills, [])
|
||||
})
|
||||
|
||||
test('tools the token cannot call are merged into the skill footnote', async (t) => {
|
||||
withTempHome(t)
|
||||
const resources = skillResources([{
|
||||
group: 'ops', groupDir: 'ops', skill: 'netx-ops', relPath: 'SKILL.md',
|
||||
text: OPS_SKILL, unavailableTools: ['execManagedNe'],
|
||||
}])
|
||||
const mock = await startMockMcp({ resources })
|
||||
t.after(() => mock.server.close())
|
||||
await refreshMcpCatalog({ apiUrl: mock.apiUrl, token: 'secret' })
|
||||
publishNetxConnection({ apiUrl: mock.apiUrl, token: 'secret', lang: 'zh', toolCallTimeoutMs: 5_000 })
|
||||
const { ctx, registered } = fakeSkillsCtx()
|
||||
await registerGroupSkills(ctx, ['ops'], 'test', { disabledByGroup: { ops: ['netx__queryNmsAlarms'] } })
|
||||
assert.match(registered[0].content, /netx__execManagedNe/)
|
||||
assert.match(registered[0].content, /netx__queryNmsAlarms/)
|
||||
assert.match(registered[0].content, /当前 token 无权使用/)
|
||||
})
|
||||
|
||||
test('cache pulled with another token is not mounted', async (t) => {
|
||||
withTempHome(t)
|
||||
replaceSkillsCache(API, [{ group: 'ops', skill: 'netx-ops', relPath: 'SKILL.md', text: OPS_SKILL }], { token: 'wide' })
|
||||
assert.ok(usableSkillsCacheDir(API, 'wide'))
|
||||
assert.equal(usableSkillsCacheDir(API, 'narrow'), '')
|
||||
assert.deepEqual(listCachedSkills(API, 'narrow').skills, [])
|
||||
publishNetxConnection({ apiUrl: API, token: 'narrow', lang: 'zh', toolCallTimeoutMs: 5_000 })
|
||||
const { ctx, registered } = fakeSkillsCtx()
|
||||
await registerGroupSkills(ctx, ['ops'], 'test')
|
||||
assert.equal(registered.length, 0)
|
||||
})
|
||||
|
||||
test('refresh prunes caches of other netx addresses unused for 30 days', (t) => {
|
||||
const home = withTempHome(t)
|
||||
const stale = join(home, 'netxops-mcp-skills', 'deadbeefdeadbeef')
|
||||
mkdirSync(stale, { recursive: true })
|
||||
writeFileSync(join(stale, '.meta.json'), JSON.stringify({ listedAt: '2020-01-01T00:00:00.000Z' }))
|
||||
const fresh = join(home, 'netxops-mcp-skills', 'feedfacefeedface')
|
||||
mkdirSync(fresh, { recursive: true })
|
||||
writeFileSync(join(fresh, '.meta.json'), JSON.stringify({ listedAt: new Date().toISOString() }))
|
||||
replaceSkillsCache(API, [], { token: 'secret' })
|
||||
assert.equal(existsSync(stale), false)
|
||||
assert.equal(existsSync(fresh), true)
|
||||
})
|
||||
|
||||
test('isInsecureRemoteApiUrl flags plain http only for non-loopback hosts', async () => {
|
||||
const { isInsecureRemoteApiUrl } = await import('../src/netx/http.ts')
|
||||
assert.equal(isInsecureRemoteApiUrl('http://127.0.0.1:8890'), false)
|
||||
assert.equal(isInsecureRemoteApiUrl('http://localhost:8890'), false)
|
||||
assert.equal(isInsecureRemoteApiUrl('http://[::1]:8890'), false)
|
||||
assert.equal(isInsecureRemoteApiUrl('https://netx.example.com'), false)
|
||||
assert.equal(isInsecureRemoteApiUrl('http://192.168.1.10:8890'), true)
|
||||
assert.equal(isInsecureRemoteApiUrl('not a url'), false)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue