mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 02:40:45 +08:00
Fix empty Bearer on netx__* by waiting for credentials.
Inject credentials before publish, read the live token per HTTP request, and surface netx_token_missing instead of a silent 401. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
02cc68bee3
commit
027f83dd00
7 changed files with 77 additions and 37 deletions
|
|
@ -31,7 +31,12 @@ export function apply(ctx: Context): void {
|
|||
return
|
||||
}
|
||||
unregister = registerNetxTools(ctx, connection)
|
||||
ctx.logger.info('netxops-tools: registered netx__* for Ops preset → %s', connection.apiUrl)
|
||||
const tokenConfigured = connection.token.trim().length > 0
|
||||
ctx.logger.info(
|
||||
'netxops-tools: registered netx__* for Ops preset → %s tokenConfigured=%s',
|
||||
connection.apiUrl,
|
||||
tokenConfigured,
|
||||
)
|
||||
}
|
||||
|
||||
remount()
|
||||
|
|
|
|||
22
src/index.ts
22
src/index.ts
|
|
@ -25,6 +25,9 @@ import { publishNetxConnection } from './netx/runtime.ts'
|
|||
/** Cordis plugin name. */
|
||||
export const name = 'netxops'
|
||||
|
||||
/** Wait for the credentials store before publishing a Bearer snapshot. */
|
||||
export const inject = ['credentials']
|
||||
|
||||
/** Settings / composition namespace (Plugins page join key). */
|
||||
export const NETXOPS_SETTINGS_NAMESPACE = 'netxops'
|
||||
|
||||
|
|
@ -101,11 +104,10 @@ export function ensureAgentPresetInstalled(logger: Context['logger']): void {
|
|||
|
||||
/**
|
||||
* Resolve bearer token from the credentials seam (or empty when unset).
|
||||
* Requires `inject: ['credentials']` so the store is live before apply.
|
||||
*/
|
||||
async function resolveToken(ctx: Context, refName: string): Promise<string> {
|
||||
const credentials = ctx.get('credentials')
|
||||
if (credentials === undefined) return ''
|
||||
const hit = await credentials.resolve(credentialRef(refName))
|
||||
const hit = await ctx.credentials.resolve(credentialRef(refName))
|
||||
return hit?.value ?? ''
|
||||
}
|
||||
|
||||
|
|
@ -160,13 +162,23 @@ export function apply(ctx: Context, config: Config = Config({})): void {
|
|||
const token = await resolveToken(ctx, current.tokenCredentialRef)
|
||||
if (gen !== generation) return
|
||||
|
||||
const apiUrl = current.apiUrl.replace(/\/$/, '')
|
||||
const tokenConfigured = token.trim().length > 0
|
||||
publishNetxConnection({
|
||||
apiUrl: current.apiUrl.replace(/\/$/, ''),
|
||||
apiUrl,
|
||||
token,
|
||||
lang: current.lang,
|
||||
toolCallTimeoutMs: current.toolCallTimeoutMs,
|
||||
})
|
||||
ctx.logger.info('netxops: published connection → %s', current.apiUrl.replace(/\/$/, ''))
|
||||
if (!tokenConfigured) {
|
||||
ctx.logger.warn(
|
||||
'netxops: published connection → %s tokenConfigured=false (set credential %s)',
|
||||
apiUrl,
|
||||
current.tokenCredentialRef,
|
||||
)
|
||||
} else {
|
||||
ctx.logger.info('netxops: published connection → %s tokenConfigured=true', apiUrl)
|
||||
}
|
||||
}).catch((error) => {
|
||||
ctx.logger.error('netxops: connection publish error: %s', error)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
|
||||
export interface NetxConnection {
|
||||
apiUrl: string
|
||||
token: string
|
||||
lang: string
|
||||
timeoutMs: number
|
||||
/** Read the latest bearer on each request (do not freeze at client creation). */
|
||||
getToken: () => string
|
||||
}
|
||||
|
||||
export type NetxJson = Record<string, unknown>
|
||||
|
|
@ -43,18 +44,12 @@ function encodeQuery(params: Record<string, string | number | boolean>): string
|
|||
}
|
||||
|
||||
/**
|
||||
* Build a client bound to the current settings/credentials snapshot.
|
||||
* @param connection - apiUrl / token / lang / default timeout.
|
||||
* Build a client bound to apiUrl / lang / timeout; Bearer is resolved per request.
|
||||
* @param connection - apiUrl / lang / timeout / getToken.
|
||||
* @returns get/post helpers that return `{ ok, data }` or `{ ok: false, error }`.
|
||||
*/
|
||||
export function createNetxClient(connection: NetxConnection) {
|
||||
const base = connection.apiUrl.replace(/\/$/, '')
|
||||
const headers: Record<string, string> = {
|
||||
accept: 'application/json',
|
||||
}
|
||||
if (connection.token.trim().length > 0) {
|
||||
headers.authorization = `Bearer ${connection.token.trim()}`
|
||||
}
|
||||
|
||||
const langParams = (): Record<string, string> => {
|
||||
const lang = connection.lang.trim().toLowerCase()
|
||||
|
|
@ -72,6 +67,15 @@ export function createNetxClient(connection: NetxConnection) {
|
|||
signal?: AbortSignal
|
||||
} = {},
|
||||
): Promise<NetxJson> {
|
||||
const token = connection.getToken().trim()
|
||||
if (token.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'netx_token_missing',
|
||||
detail: 'Set credential NETX_API_TOKEN (Plugins → Netx Ops or scripts/set-netx-token).',
|
||||
}
|
||||
}
|
||||
|
||||
const merged: Record<string, string | number | boolean> = { ...langParams(), ...options.params }
|
||||
const url = `${base}${path}${encodeQuery(merged)}`
|
||||
const timeoutMs = options.timeoutMs ?? connection.timeoutMs
|
||||
|
|
@ -80,11 +84,14 @@ export function createNetxClient(connection: NetxConnection) {
|
|||
const onOuterAbort = () => { controller.abort() }
|
||||
options.signal?.addEventListener('abort', onOuterAbort, { once: true })
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
accept: 'application/json',
|
||||
authorization: `Bearer ${token}`,
|
||||
}
|
||||
if (options.body !== undefined) headers['content-type'] = 'application/json'
|
||||
const init: RequestInit = {
|
||||
method,
|
||||
headers: options.body === undefined
|
||||
? headers
|
||||
: { ...headers, 'content-type': 'application/json' },
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
}
|
||||
if (options.body !== undefined) init.body = JSON.stringify(options.body)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { createNetxClient, type NetxClient, type NetxJson } from './http.ts'
|
||||
import { getNetxConnection } from './runtime.ts'
|
||||
import * as H from './handlers.ts'
|
||||
|
||||
export interface NetxToolConnection {
|
||||
|
|
@ -61,16 +62,17 @@ function tool(
|
|||
|
||||
/**
|
||||
* Register all Netx Ops tools against the current connection snapshot.
|
||||
* Bearer is read from the live process store on each request (not frozen here).
|
||||
* @param ctx - host context with `tools`.
|
||||
* @param connection - apiUrl / token / lang / timeout.
|
||||
* @param connection - apiUrl / token / lang / timeout (apiUrl/lang/timeout used for client base).
|
||||
* @returns disposer that unregisters every tool.
|
||||
*/
|
||||
export function registerNetxTools(ctx: Context, connection: NetxToolConnection): () => void {
|
||||
const client = createNetxClient({
|
||||
apiUrl: connection.apiUrl,
|
||||
token: connection.token,
|
||||
lang: connection.lang,
|
||||
timeoutMs: Math.min(connection.toolCallTimeoutMs, 45_000),
|
||||
getToken: () => getNetxConnection()?.token ?? '',
|
||||
})
|
||||
const getClient = () => client
|
||||
const t = connection.toolCallTimeoutMs
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue