From 027f83dd009268e4cd4f9493266d214d22793258 Mon Sep 17 00:00:00 2001 From: oliver Date: Sat, 5 Sep 2026 02:11:20 +0800 Subject: [PATCH] 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 --- lib/agent-tools.js | 29 +++++++++++++++++++---------- lib/index.js | 17 +++++++++++------ package.json | 2 +- src/agent-tools.ts | 7 ++++++- src/index.ts | 22 +++++++++++++++++----- src/netx/http.ts | 31 +++++++++++++++++++------------ src/netx/tools.ts | 6 ++++-- 7 files changed, 77 insertions(+), 37 deletions(-) diff --git a/lib/agent-tools.js b/lib/agent-tools.js index 7b4fc69..eaf1982 100644 --- a/lib/agent-tools.js +++ b/lib/agent-tools.js @@ -57,12 +57,6 @@ function encodeQuery(params) { } function createNetxClient(connection) { const base = connection.apiUrl.replace(/\/$/, ""); - const headers = { - accept: "application/json" - }; - if (connection.token.trim().length > 0) { - headers.authorization = `Bearer ${connection.token.trim()}`; - } const langParams = () => { const lang = connection.lang.trim().toLowerCase(); if (lang.startsWith("en")) @@ -70,6 +64,14 @@ function createNetxClient(connection) { return {}; }; async function request(method, path, options = {}) { + 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 = { ...langParams(), ...options.params }; const url = `${base}${path}${encodeQuery(merged)}`; const timeoutMs = options.timeoutMs ?? connection.timeoutMs; @@ -82,9 +84,15 @@ function createNetxClient(connection) { }; options.signal?.addEventListener("abort", onOuterAbort, { once: true }); try { + const headers = { + accept: "application/json", + authorization: `Bearer ${token}` + }; + if (options.body !== undefined) + headers["content-type"] = "application/json"; const init = { method, - headers: options.body === undefined ? headers : { ...headers, "content-type": "application/json" }, + headers, signal: controller.signal }; if (options.body !== undefined) @@ -515,9 +523,9 @@ function tool(name, description, parameters, handler, getClient, timeoutMs) { function registerNetxTools(ctx, connection) { const client = createNetxClient({ apiUrl: connection.apiUrl, - token: connection.token, lang: connection.lang, - timeoutMs: Math.min(connection.toolCallTimeoutMs, 45000) + timeoutMs: Math.min(connection.toolCallTimeoutMs, 45000), + getToken: () => getNetxConnection()?.token ?? "" }); const getClient = () => client; const t = connection.toolCallTimeoutMs; @@ -662,7 +670,8 @@ function apply(ctx) { 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(); const stopWatch = watchNetxConnection(() => { diff --git a/lib/index.js b/lib/index.js index aa052bd..5fe777a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -27,6 +27,7 @@ function publishNetxConnection(next) { // src/index.ts var name = "netxops"; +var inject = ["credentials"]; var NETXOPS_SETTINGS_NAMESPACE = "netxops"; var NETXOPS_PRESET_ID = "netxops"; var DEFAULT_TOKEN_REF = "NETX_API_TOKEN"; @@ -69,10 +70,7 @@ function ensureAgentPresetInstalled(logger) { } } async function resolveToken(ctx, refName) { - 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 ?? ""; } function installNetxopsSettings(ctx, entry, hooks) { @@ -99,13 +97,19 @@ function apply(ctx, config = Config({})) { 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); }); @@ -129,6 +133,7 @@ function apply(ctx, config = Config({})) { } export { name, + inject, ensureAgentPresetInstalled, apply, NETXOPS_SETTINGS_NAMESPACE, diff --git a/package.json b/package.json index 831acd7..38ac26d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-netxops", - "version": "0.1.13", + "version": "0.1.14", "description": "DeepSeek Harness Netx Ops: Ops-scoped netx__* REST tools + Plugins settings card + agent preset", "license": "MIT", "type": "module", diff --git a/src/agent-tools.ts b/src/agent-tools.ts index cfde2c3..2e9ec65 100644 --- a/src/agent-tools.ts +++ b/src/agent-tools.ts @@ -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() diff --git a/src/index.ts b/src/index.ts index 7ee8d37..e29c6c8 100644 --- a/src/index.ts +++ b/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 { - 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) }) diff --git a/src/netx/http.ts b/src/netx/http.ts index 10160c9..a1d4f5b 100644 --- a/src/netx/http.ts +++ b/src/netx/http.ts @@ -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 @@ -43,18 +44,12 @@ function encodeQuery(params: Record): 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 = { - accept: 'application/json', - } - if (connection.token.trim().length > 0) { - headers.authorization = `Bearer ${connection.token.trim()}` - } const langParams = (): Record => { const lang = connection.lang.trim().toLowerCase() @@ -72,6 +67,15 @@ export function createNetxClient(connection: NetxConnection) { signal?: AbortSignal } = {}, ): Promise { + 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 = { ...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 = { + 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) diff --git a/src/netx/tools.ts b/src/netx/tools.ts index ac0c289..768772a 100644 --- a/src/netx/tools.ts +++ b/src/netx/tools.ts @@ -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