Do not hard-inject remote.credentials on shipped dsh.

0.1.1-rc.2 remotes never mounts credentials; soft-inject unlocks the token field when available, otherwise point users at set-netx-token.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-04 22:43:46 +08:00
parent 69a4820369
commit 99ded2fe2d
6 changed files with 143 additions and 28 deletions

View file

@ -251,7 +251,7 @@ function NetxopsCard(props) {
}), }),
/* @__PURE__ */ jsx_runtime.jsx("p", { /* @__PURE__ */ jsx_runtime.jsx("p", {
className: "dsh-nx-hint", className: "dsh-nx-hint",
children: t("apiTokenHint") children: state.apiTokenRemoteReady ? t("apiTokenHint") : t("apiTokenUnavailable")
}) })
] ]
}), }),
@ -533,7 +533,12 @@ class NetxopsCardController {
ctx; ctx;
form; form;
store; store;
credential = { ref: "", configured: false, writable: true }; credential = {
ref: "",
configured: false,
writable: false,
remoteReady: false
};
constructor(scope, ctx) { constructor(scope, ctx) {
this.scope = scope; this.scope = scope;
this.ctx = ctx; this.ctx = ctx;
@ -544,6 +549,18 @@ class NetxopsCardController {
}); });
this.readCredential(); 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() { projection() {
return { return {
...this.form.shell(), ...this.form.shell(),
@ -552,25 +569,43 @@ class NetxopsCardController {
pythonCommand: this.form.field("pythonCommand"), pythonCommand: this.form.field("pythonCommand"),
apiToken: this.form.field(API_TOKEN_FIELD), apiToken: this.form.field(API_TOKEN_FIELD),
apiTokenConfigured: this.credential.configured, 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() { async readCredential() {
const ref = refOf(this.scope.getSnapshot()); 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) { 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()); 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())) if (!response.ok || ref !== refOf(this.scope.getSnapshot()))
return; return;
const view = response.value[ref]; const view = response.value?.[ref];
const next = { const next = {
ref, ref,
configured: view?.configured ?? false, 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; return;
this.credential = next; this.credential = next;
this.store.set(this.projection()); this.store.set(this.projection());
@ -584,7 +619,10 @@ class NetxopsCardController {
return { hooks: { netxopsCard: this.store }, ...this.form.actions() }; return { hooks: { netxopsCard: this.store }, ...this.form.actions() };
} }
async writeToken(value) { 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(); await this.readCredential();
return this.credential.configured; return this.credential.configured;
} }
@ -606,6 +644,7 @@ var en = {
pythonCommandHint: "Executable that can run `python -m netx_mcp`.", pythonCommandHint: "Executable that can run `python -m netx_mcp`.",
apiToken: "API token", apiToken: "API token",
apiTokenHint: "Stored as credential NETX_API_TOKEN (never written into settings). Leave blank to keep the current 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", apiTokenSet: "Configured",
apiTokenUnset: "Not set", apiTokenUnset: "Not set",
overridden: "Overridden", overridden: "Overridden",
@ -631,6 +670,7 @@ var zh = {
pythonCommandHint: "能执行 `python -m netx_mcp` 的解释器。", pythonCommandHint: "能执行 `python -m netx_mcp` 的解释器。",
apiToken: "API Token", apiToken: "API Token",
apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。", apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。",
apiTokenUnavailable: "当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。",
apiTokenSet: "已配置", apiTokenSet: "已配置",
apiTokenUnset: "未设置", apiTokenUnset: "未设置",
overridden: "已覆盖", overridden: "已覆盖",
@ -652,15 +692,23 @@ var inject = [
"slots", "slots",
"locale", "locale",
"remote", "remote",
"remote.credentials",
"settingsScope" "settingsScope"
]; ];
function apply(ctx) { function apply(ctx) {
ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), "netxops: locales"); ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), "netxops: locales");
const card = new NetxopsCardController(ctx.settingsScope.bind({ namespace: NETXOPS_NS }), ctx); const card = new NetxopsCardController(ctx.settingsScope.bind({ namespace: NETXOPS_NS }), ctx);
ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => { ctx.inject(["remote.credentials"], (credCtx) => {
card.refreshCredential(ref); card.setCredentialsAvailable(true);
}), "netxops: credential invalidations"); 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({ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
name: "settings.plugin.item", name: "settings.plugin.item",
key: NETXOPS_NS, key: NETXOPS_NS,

View file

@ -1,6 +1,6 @@
{ {
"name": "dsh-netxops", "name": "dsh-netxops",
"version": "0.1.7", "version": "0.1.8",
"description": "DeepSeek Harness Netx Ops: host bridge + Plugins settings card + agent preset", "description": "DeepSeek Harness Netx Ops: host bridge + Plugins settings card + agent preset",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",

View file

@ -119,7 +119,9 @@ export function NetxopsCard(props: NetxopsCardProps) {
disabled={!state.apiTokenWritable} disabled={!state.apiTokenWritable}
onChange={(event) => { props.edit('apiToken', event.target.value) }} onChange={(event) => { props.edit('apiToken', event.target.value) }}
/> />
<p className="dsh-nx-hint">{t('apiTokenHint')}</p> <p className="dsh-nx-hint">
{state.apiTokenRemoteReady ? t('apiTokenHint') : t('apiTokenUnavailable')}
</p>
</div> </div>
<ValueField <ValueField
id="netxops-api-url" id="netxops-api-url"

View file

@ -1,5 +1,5 @@
/** /**
* Staged form over settings namespace `netxops` + credential NETX_API_TOKEN. * Staged form over settings namespace `netxops` + optional credential NETX_API_TOKEN.
*/ */
import type { Context as ClientContext } from '@deepseek-ai/cordis' import type { Context as ClientContext } from '@deepseek-ai/cordis'
@ -26,6 +26,7 @@ interface CredentialState {
ref: string ref: string
configured: boolean configured: boolean
writable: boolean writable: boolean
remoteReady: boolean
} }
export interface NetxopsCardState extends CardShell { export interface NetxopsCardState extends CardShell {
@ -35,6 +36,7 @@ export interface NetxopsCardState extends CardShell {
apiToken: CardFieldState apiToken: CardFieldState
apiTokenConfigured: boolean apiTokenConfigured: boolean
apiTokenWritable: boolean apiTokenWritable: boolean
apiTokenRemoteReady: boolean
} }
export interface NetxopsCardFace extends CardActions { export interface NetxopsCardFace extends CardActions {
@ -43,10 +45,20 @@ export interface NetxopsCardFace extends CardActions {
} }
} }
type CredentialsRemote = {
describe: (refs: string[]) => Promise<{ ok: boolean; value?: Record<string, { configured?: boolean; writable?: boolean } | undefined> }>
set: (ref: string, value: string) => Promise<unknown>
}
export class NetxopsCardController { export class NetxopsCardController {
private readonly form: CardForm<NetxopsSettings> private readonly form: CardForm<NetxopsSettings>
private readonly store: SnapshotStore<NetxopsCardState> private readonly store: SnapshotStore<NetxopsCardState>
private credential: CredentialState = { ref: '', configured: false, writable: true } private credential: CredentialState = {
ref: '',
configured: false,
writable: false,
remoteReady: false,
}
constructor( constructor(
private readonly scope: SettingsScope<NetxopsSettings>, private readonly scope: SettingsScope<NetxopsSettings>,
@ -62,6 +74,18 @@ export class NetxopsCardController {
void this.readCredential() 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 { private projection(): NetxopsCardState {
return { return {
...this.form.shell(), ...this.form.shell(),
@ -70,25 +94,48 @@ export class NetxopsCardController {
pythonCommand: this.form.field('pythonCommand'), pythonCommand: this.form.field('pythonCommand'),
apiToken: this.form.field(API_TOKEN_FIELD), apiToken: this.form.field(API_TOKEN_FIELD),
apiTokenConfigured: this.credential.configured, 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<void> { private async readCredential(): Promise<void> {
const ref = refOf(this.scope.getSnapshot()) 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) { 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()) 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 if (!response.ok || ref !== refOf(this.scope.getSnapshot())) return
const view = response.value[ref] const view = response.value?.[ref]
const next: CredentialState = { const next: CredentialState = {
ref, ref,
configured: view?.configured ?? false, 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) return if (
next.configured === this.credential.configured
&& next.writable === this.credential.writable
&& next.remoteReady === this.credential.remoteReady
) return
this.credential = next this.credential = next
this.store.set(this.projection()) this.store.set(this.projection())
} }
@ -103,7 +150,9 @@ export class NetxopsCardController {
} }
private async writeToken(value: string): Promise<boolean> { private async writeToken(value: string): Promise<boolean> {
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() await this.readCredential()
return this.credential.configured return this.credential.configured
} }

View file

@ -1,5 +1,9 @@
/** /**
* Browser half — Settings → Plugins → Netx Ops card. * 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' 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' const LOCALE_NS = 'settings.netxops'
/** Match shipped ui-settings-plugins inject (no remote.credentials). */
export const inject = [ export const inject = [
'slots', 'slots',
'locale', 'locale',
'remote', 'remote',
'remote.credentials',
'settingsScope', 'settingsScope',
] ]
@ -36,10 +40,19 @@ export function apply(ctx: ClientContext): void {
ctx, ctx,
) )
ctx.effect( // Optional: newer remotes that mount credentials unlock the token field.
() => ctx.remote.$on('credentials/reference-updated', (ref) => { card.refreshCredential(ref) }), ctx.inject(['remote.credentials'], (credCtx) => {
'netxops: credential invalidations', 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({ ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
name: 'settings.plugin.item', name: 'settings.plugin.item',

View file

@ -11,6 +11,7 @@ export type NetxopsLocaleKey =
| 'pythonCommandHint' | 'pythonCommandHint'
| 'apiToken' | 'apiToken'
| 'apiTokenHint' | 'apiTokenHint'
| 'apiTokenUnavailable'
| 'apiTokenSet' | 'apiTokenSet'
| 'apiTokenUnset' | 'apiTokenUnset'
| 'overridden' | 'overridden'
@ -36,6 +37,7 @@ export const en: Record<NetxopsLocaleKey, string> = {
pythonCommandHint: 'Executable that can run `python -m netx_mcp`.', pythonCommandHint: 'Executable that can run `python -m netx_mcp`.',
apiToken: 'API token', apiToken: 'API token',
apiTokenHint: 'Stored as credential NETX_API_TOKEN (never written into settings). Leave blank to keep the current 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', apiTokenSet: 'Configured',
apiTokenUnset: 'Not set', apiTokenUnset: 'Not set',
overridden: 'Overridden', overridden: 'Overridden',
@ -62,6 +64,7 @@ export const zh: Record<NetxopsLocaleKey, string> = {
pythonCommandHint: '能执行 `python -m netx_mcp` 的解释器。', pythonCommandHint: '能执行 `python -m netx_mcp` 的解释器。',
apiToken: 'API Token', apiToken: 'API Token',
apiTokenHint: '写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。', apiTokenHint: '写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。',
apiTokenUnavailable: '当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。',
apiTokenSet: '已配置', apiTokenSet: '已配置',
apiTokenUnset: '未设置', apiTokenUnset: '未设置',
overridden: '已覆盖', overridden: '已覆盖',