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
|
|
@ -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