diff --git a/lib/client.js b/lib/client.js
index 1ed115c..9c267f5 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -251,7 +251,7 @@ function NetxopsCard(props) {
}),
/* @__PURE__ */ jsx_runtime.jsx("p", {
className: "dsh-nx-hint",
- children: t("apiTokenHint")
+ children: state.apiTokenRemoteReady ? t("apiTokenHint") : t("apiTokenUnavailable")
})
]
}),
@@ -533,7 +533,12 @@ class NetxopsCardController {
ctx;
form;
store;
- credential = { ref: "", configured: false, writable: true };
+ credential = {
+ ref: "",
+ configured: false,
+ writable: false,
+ remoteReady: false
+ };
constructor(scope, ctx) {
this.scope = scope;
this.ctx = ctx;
@@ -544,6 +549,18 @@ class NetxopsCardController {
});
this.readCredential();
}
+ setCredentialsAvailable(ready) {
+ if (this.credential.remoteReady === ready)
+ return;
+ this.credential = {
+ ...this.credential,
+ remoteReady: ready,
+ writable: ready
+ };
+ this.store.set(this.projection());
+ if (ready)
+ this.readCredential();
+ }
projection() {
return {
...this.form.shell(),
@@ -552,25 +569,43 @@ class NetxopsCardController {
pythonCommand: this.form.field("pythonCommand"),
apiToken: this.form.field(API_TOKEN_FIELD),
apiTokenConfigured: this.credential.configured,
- apiTokenWritable: this.credential.writable
+ apiTokenWritable: this.credential.remoteReady && this.credential.writable,
+ apiTokenRemoteReady: this.credential.remoteReady
};
}
+ credentials() {
+ return this.ctx.get("remote.credentials");
+ }
async readCredential() {
const ref = refOf(this.scope.getSnapshot());
+ const api = this.credentials();
+ if (api === undefined) {
+ if (ref !== this.credential.ref || this.credential.remoteReady) {
+ this.credential = {
+ ref,
+ configured: false,
+ writable: false,
+ remoteReady: false
+ };
+ this.store.set(this.projection());
+ }
+ return;
+ }
if (ref !== this.credential.ref) {
- this.credential = { ref, configured: false, writable: true };
+ this.credential = { ref, configured: false, writable: true, remoteReady: true };
this.store.set(this.projection());
}
- const response = await this.ctx.remote.credentials.describe([ref]);
+ const response = await api.describe([ref]);
if (!response.ok || ref !== refOf(this.scope.getSnapshot()))
return;
- const view = response.value[ref];
+ const view = response.value?.[ref];
const next = {
ref,
configured: view?.configured ?? false,
- writable: view?.writable ?? true
+ writable: view?.writable ?? true,
+ remoteReady: true
};
- if (next.configured === this.credential.configured && next.writable === this.credential.writable)
+ if (next.configured === this.credential.configured && next.writable === this.credential.writable && next.remoteReady === this.credential.remoteReady)
return;
this.credential = next;
this.store.set(this.projection());
@@ -584,7 +619,10 @@ class NetxopsCardController {
return { hooks: { netxopsCard: this.store }, ...this.form.actions() };
}
async writeToken(value) {
- await this.ctx.remote.credentials.set(refOf(this.scope.getSnapshot()), value);
+ const api = this.credentials();
+ if (api === undefined)
+ return false;
+ await api.set(refOf(this.scope.getSnapshot()), value);
await this.readCredential();
return this.credential.configured;
}
@@ -606,6 +644,7 @@ var en = {
pythonCommandHint: "Executable that can run `python -m netx_mcp`.",
apiToken: "API token",
apiTokenHint: "Stored as credential NETX_API_TOKEN (never written into settings). Leave blank to keep the current token.",
+ apiTokenUnavailable: "This DSH build does not expose remote.credentials. Set the token with scripts/set-netx-token.ps1 (or .sh), then restart is not required if credentials are watched.",
apiTokenSet: "Configured",
apiTokenUnset: "Not set",
overridden: "Overridden",
@@ -631,6 +670,7 @@ var zh = {
pythonCommandHint: "能执行 `python -m netx_mcp` 的解释器。",
apiToken: "API Token",
apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。",
+ apiTokenUnavailable: "当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。",
apiTokenSet: "已配置",
apiTokenUnset: "未设置",
overridden: "已覆盖",
@@ -652,15 +692,23 @@ var inject = [
"slots",
"locale",
"remote",
- "remote.credentials",
"settingsScope"
];
function apply(ctx) {
ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), "netxops: locales");
const card = new NetxopsCardController(ctx.settingsScope.bind({ namespace: NETXOPS_NS }), ctx);
- ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => {
- card.refreshCredential(ref);
- }), "netxops: credential invalidations");
+ ctx.inject(["remote.credentials"], (credCtx) => {
+ card.setCredentialsAvailable(true);
+ credCtx.effect(() => {
+ const off = credCtx.remote.$on("credentials/reference-updated", (ref) => {
+ card.refreshCredential(String(ref));
+ });
+ return () => {
+ off();
+ card.setCredentialsAvailable(false);
+ };
+ }, "netxops: credential invalidations");
+ });
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
name: "settings.plugin.item",
key: NETXOPS_NS,
diff --git a/package.json b/package.json
index fccb823..ca2d43b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dsh-netxops",
- "version": "0.1.7",
+ "version": "0.1.8",
"description": "DeepSeek Harness Netx Ops: host bridge + Plugins settings card + agent preset",
"license": "MIT",
"type": "module",
diff --git a/src/client/NetxopsCard.tsx b/src/client/NetxopsCard.tsx
index 28bd6b8..dd75e7a 100644
--- a/src/client/NetxopsCard.tsx
+++ b/src/client/NetxopsCard.tsx
@@ -119,7 +119,9 @@ export function NetxopsCard(props: NetxopsCardProps) {
disabled={!state.apiTokenWritable}
onChange={(event) => { props.edit('apiToken', event.target.value) }}
/>
-
{t('apiTokenHint')}
+
+ {state.apiTokenRemoteReady ? t('apiTokenHint') : t('apiTokenUnavailable')}
+
Promise<{ ok: boolean; value?: Record }>
+ set: (ref: string, value: string) => Promise
+}
+
export class NetxopsCardController {
private readonly form: CardForm
private readonly store: SnapshotStore
- private credential: CredentialState = { ref: '', configured: false, writable: true }
+ private credential: CredentialState = {
+ ref: '',
+ configured: false,
+ writable: false,
+ remoteReady: false,
+ }
constructor(
private readonly scope: SettingsScope,
@@ -62,6 +74,18 @@ export class NetxopsCardController {
void this.readCredential()
}
+ /** Toggle when soft-injected `remote.credentials` arrives / leaves. */
+ setCredentialsAvailable(ready: boolean): void {
+ if (this.credential.remoteReady === ready) return
+ this.credential = {
+ ...this.credential,
+ remoteReady: ready,
+ writable: ready,
+ }
+ this.store.set(this.projection())
+ if (ready) void this.readCredential()
+ }
+
private projection(): NetxopsCardState {
return {
...this.form.shell(),
@@ -70,25 +94,48 @@ export class NetxopsCardController {
pythonCommand: this.form.field('pythonCommand'),
apiToken: this.form.field(API_TOKEN_FIELD),
apiTokenConfigured: this.credential.configured,
- apiTokenWritable: this.credential.writable,
+ apiTokenWritable: this.credential.remoteReady && this.credential.writable,
+ apiTokenRemoteReady: this.credential.remoteReady,
}
}
+ private credentials(): CredentialsRemote | undefined {
+ return this.ctx.get('remote.credentials') as CredentialsRemote | undefined
+ }
+
private async readCredential(): Promise {
const ref = refOf(this.scope.getSnapshot())
+ const api = this.credentials()
+ if (api === undefined) {
+ if (ref !== this.credential.ref || this.credential.remoteReady) {
+ this.credential = {
+ ref,
+ configured: false,
+ writable: false,
+ remoteReady: false,
+ }
+ this.store.set(this.projection())
+ }
+ return
+ }
if (ref !== this.credential.ref) {
- this.credential = { ref, configured: false, writable: true }
+ this.credential = { ref, configured: false, writable: true, remoteReady: true }
this.store.set(this.projection())
}
- const response = await this.ctx.remote.credentials.describe([ref])
+ const response = await api.describe([ref])
if (!response.ok || ref !== refOf(this.scope.getSnapshot())) return
- const view = response.value[ref]
+ const view = response.value?.[ref]
const next: CredentialState = {
ref,
configured: view?.configured ?? false,
writable: view?.writable ?? true,
+ remoteReady: true,
}
- if (next.configured === this.credential.configured && next.writable === this.credential.writable) return
+ if (
+ next.configured === this.credential.configured
+ && next.writable === this.credential.writable
+ && next.remoteReady === this.credential.remoteReady
+ ) return
this.credential = next
this.store.set(this.projection())
}
@@ -103,7 +150,9 @@ export class NetxopsCardController {
}
private async writeToken(value: string): Promise {
- await this.ctx.remote.credentials.set(refOf(this.scope.getSnapshot()), value)
+ const api = this.credentials()
+ if (api === undefined) return false
+ await api.set(refOf(this.scope.getSnapshot()), value)
await this.readCredential()
return this.credential.configured
}
diff --git a/src/client/index.ts b/src/client/index.ts
index f7203d4..993da9d 100644
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@ -1,5 +1,9 @@
/**
* Browser half — Settings → Plugins → Netx Ops card.
+ *
+ * Do not hard-inject `remote.credentials`: shipped `@deepseek-ai/dsh` 0.1.1-rc.2
+ * remotes assembly does not mount that namespace (Models/Plugins cards only
+ * inject `remote`). Soft-inject when a newer Host provides it.
*/
import type { Context as ClientContext } from '@deepseek-ai/cordis'
@@ -20,11 +24,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
const LOCALE_NS = 'settings.netxops'
+/** Match shipped ui-settings-plugins inject (no remote.credentials). */
export const inject = [
'slots',
'locale',
'remote',
- 'remote.credentials',
'settingsScope',
]
@@ -36,10 +40,19 @@ export function apply(ctx: ClientContext): void {
ctx,
)
- ctx.effect(
- () => ctx.remote.$on('credentials/reference-updated', (ref) => { card.refreshCredential(ref) }),
- 'netxops: credential invalidations',
- )
+ // Optional: newer remotes that mount credentials unlock the token field.
+ ctx.inject(['remote.credentials'], (credCtx) => {
+ card.setCredentialsAvailable(true)
+ credCtx.effect(() => {
+ const off = credCtx.remote.$on('credentials/reference-updated', (ref) => {
+ card.refreshCredential(String(ref))
+ })
+ return () => {
+ off()
+ card.setCredentialsAvailable(false)
+ }
+ }, 'netxops: credential invalidations')
+ })
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
name: 'settings.plugin.item',
diff --git a/src/client/locales.ts b/src/client/locales.ts
index 9ab88f7..752b75a 100644
--- a/src/client/locales.ts
+++ b/src/client/locales.ts
@@ -11,6 +11,7 @@ export type NetxopsLocaleKey =
| 'pythonCommandHint'
| 'apiToken'
| 'apiTokenHint'
+ | 'apiTokenUnavailable'
| 'apiTokenSet'
| 'apiTokenUnset'
| 'overridden'
@@ -36,6 +37,7 @@ export const en: Record = {
pythonCommandHint: 'Executable that can run `python -m netx_mcp`.',
apiToken: 'API token',
apiTokenHint: 'Stored as credential NETX_API_TOKEN (never written into settings). Leave blank to keep the current token.',
+ apiTokenUnavailable: 'This DSH build does not expose remote.credentials. Set the token with scripts/set-netx-token.ps1 (or .sh), then restart is not required if credentials are watched.',
apiTokenSet: 'Configured',
apiTokenUnset: 'Not set',
overridden: 'Overridden',
@@ -62,6 +64,7 @@ export const zh: Record = {
pythonCommandHint: '能执行 `python -m netx_mcp` 的解释器。',
apiToken: 'API Token',
apiTokenHint: '写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。',
+ apiTokenUnavailable: '当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。',
apiTokenSet: '已配置',
apiTokenUnset: '未设置',
overridden: '已覆盖',