mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 03:53:15 +08:00
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:
parent
69a4820369
commit
99ded2fe2d
6 changed files with 143 additions and 28 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -119,7 +119,9 @@ export function NetxopsCard(props: NetxopsCardProps) {
|
|||
disabled={!state.apiTokenWritable}
|
||||
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>
|
||||
<ValueField
|
||||
id="netxops-api-url"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -26,6 +26,7 @@ interface CredentialState {
|
|||
ref: string
|
||||
configured: boolean
|
||||
writable: boolean
|
||||
remoteReady: boolean
|
||||
}
|
||||
|
||||
export interface NetxopsCardState extends CardShell {
|
||||
|
|
@ -35,6 +36,7 @@ export interface NetxopsCardState extends CardShell {
|
|||
apiToken: CardFieldState
|
||||
apiTokenConfigured: boolean
|
||||
apiTokenWritable: boolean
|
||||
apiTokenRemoteReady: boolean
|
||||
}
|
||||
|
||||
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 {
|
||||
private readonly form: CardForm<NetxopsSettings>
|
||||
private readonly store: SnapshotStore<NetxopsCardState>
|
||||
private credential: CredentialState = { ref: '', configured: false, writable: true }
|
||||
private credential: CredentialState = {
|
||||
ref: '',
|
||||
configured: false,
|
||||
writable: false,
|
||||
remoteReady: false,
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly scope: SettingsScope<NetxopsSettings>,
|
||||
|
|
@ -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<void> {
|
||||
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<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()
|
||||
return this.credential.configured
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export type NetxopsLocaleKey =
|
|||
| 'pythonCommandHint'
|
||||
| 'apiToken'
|
||||
| 'apiTokenHint'
|
||||
| 'apiTokenUnavailable'
|
||||
| 'apiTokenSet'
|
||||
| 'apiTokenUnset'
|
||||
| 'overridden'
|
||||
|
|
@ -36,6 +37,7 @@ export const en: Record<NetxopsLocaleKey, string> = {
|
|||
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<NetxopsLocaleKey, string> = {
|
|||
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: '已覆盖',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue