dsh-server-connector 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/LICENSE +0 -0
- package/README.md +49 -7
- package/cordis.patch.yml +6 -6
- package/icon.svg +15 -15
- package/lib/audit.js +469 -455
- package/lib/client.js +2389 -1728
- package/lib/config.js +89 -0
- package/lib/index.js +316 -266
- package/lib/known-hosts.js +377 -377
- package/lib/pending.js +766 -753
- package/lib/policy.js +590 -590
- package/lib/pool.js +1496 -1464
- package/lib/registry.js +968 -968
- package/lib/routes.js +14 -13
- package/lib/tools.js +1367 -1357
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +105 -105
package/lib/index.js
CHANGED
|
@@ -1,266 +1,316 @@
|
|
|
1
|
-
// dsh-server-connector — Host half.
|
|
2
|
-
//
|
|
3
|
-
// Unit map (doc §2.1):
|
|
4
|
-
//
|
|
5
|
-
// policy.js built-in high-risk catalog + per-server decisions (§4)
|
|
6
|
-
// registry.js servers.json CRUD (§3.1)
|
|
7
|
-
// known-hosts.js known_hosts.json fingerprints (§3.2)
|
|
8
|
-
// pool.js ssh2 connection pool + execution (§7)
|
|
9
|
-
// audit.js audit.log JSONL append/tail (§3.4)
|
|
10
|
-
// pending.js pending.json two-step queue (§3.3, §8.3)
|
|
11
|
-
// tools.js the six srv_* tools + the §5.3 system-prompt section (§5, §6)
|
|
12
|
-
//
|
|
13
|
-
// This entry wires those units, the plugin's lifecycle and the Client half's
|
|
14
|
-
// JSON routes (doc §8.5, lib/routes.js). The browser bundle itself is
|
|
15
|
-
// lib/client.js, loaded by the client module system, not by this file.
|
|
16
|
-
//
|
|
17
|
-
// Config MUST be a @deepseek-ai/schemastery schema, never zod: the loader
|
|
18
|
-
// rejects a row whose Config is a foreign schema (dsh-remote records this as
|
|
19
|
-
// "zod rejects the undefined row config").
|
|
20
|
-
|
|
21
|
-
import z from '@deepseek-ai/schemastery'
|
|
22
|
-
|
|
23
|
-
import { AuditLog, defaultAuditFile } from './audit.js'
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* @
|
|
83
|
-
* @
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
* @param {
|
|
109
|
-
* has the same `(config, options)` signature.
|
|
110
|
-
* @param {{file?: string, env?: NodeJS.ProcessEnv}} [options]
|
|
111
|
-
* @returns {
|
|
112
|
-
*/
|
|
113
|
-
export function
|
|
114
|
-
void config
|
|
115
|
-
return new
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Build the
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
*
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
)
|
|
266
|
-
|
|
1
|
+
// dsh-server-connector — Host half.
|
|
2
|
+
//
|
|
3
|
+
// Unit map (doc §2.1):
|
|
4
|
+
//
|
|
5
|
+
// policy.js built-in high-risk catalog + per-server decisions (§4)
|
|
6
|
+
// registry.js servers.json CRUD (§3.1)
|
|
7
|
+
// known-hosts.js known_hosts.json fingerprints (§3.2)
|
|
8
|
+
// pool.js ssh2 connection pool + execution (§7)
|
|
9
|
+
// audit.js audit.log JSONL append/tail (§3.4)
|
|
10
|
+
// pending.js pending.json two-step queue (§3.3, §8.3)
|
|
11
|
+
// tools.js the six srv_* tools + the §5.3 system-prompt section (§5, §6)
|
|
12
|
+
//
|
|
13
|
+
// This entry wires those units, the plugin's lifecycle and the Client half's
|
|
14
|
+
// JSON routes (doc §8.5, lib/routes.js). The browser bundle itself is
|
|
15
|
+
// lib/client.js, loaded by the client module system, not by this file.
|
|
16
|
+
//
|
|
17
|
+
// Config MUST be a @deepseek-ai/schemastery schema, never zod: the loader
|
|
18
|
+
// rejects a row whose Config is a foreign schema (dsh-remote records this as
|
|
19
|
+
// "zod rejects the undefined row config").
|
|
20
|
+
|
|
21
|
+
import z from '@deepseek-ai/schemastery'
|
|
22
|
+
|
|
23
|
+
import { AuditLog, defaultAuditFile } from './audit.js'
|
|
24
|
+
import { readConfig } from './config.js'
|
|
25
|
+
import { KnownHosts, defaultKnownHostsFile } from './known-hosts.js'
|
|
26
|
+
import { PendingQueue, createPendingApplier, defaultPendingFile } from './pending.js'
|
|
27
|
+
import { Pool } from './pool.js'
|
|
28
|
+
import { Registry, defaultServersFile } from './registry.js'
|
|
29
|
+
import { registerRoutes } from './routes.js'
|
|
30
|
+
import { TOOL_NAMES, ToolSurface } from './tools.js'
|
|
31
|
+
|
|
32
|
+
export const name = 'dsh-server-connector'
|
|
33
|
+
|
|
34
|
+
// Hard dependencies of the finished plugin (doc §9.4). Declared from phase 1 so
|
|
35
|
+
// the row's requirements are visible; credentials / jobs / webServer are read
|
|
36
|
+
// optionally with ctx.get() once those units exist.
|
|
37
|
+
export const inject = ['tools', 'systemPrompt']
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Global configuration (doc §10). Every key has a default, so an empty row config validates.
|
|
41
|
+
*
|
|
42
|
+
* Every key is `.volatile()`, and that is not decoration. DSH 0.2.2 exposes a
|
|
43
|
+
* plugin's settings form only through `volatileForm()`, which keeps a field only
|
|
44
|
+
* when it is marked volatile (`dsh-settings/lib/index.js`); a schema with no
|
|
45
|
+
* volatile field projects to `undefined`, the settings page lists no namespace
|
|
46
|
+
* for this row, and `ctx.configForms.get('dsh-server-connector')` stays
|
|
47
|
+
* `unavailable` forever. That is why the settings page used to show nine
|
|
48
|
+
* read-only numbers with no way to edit them — the row was never projected.
|
|
49
|
+
*
|
|
50
|
+
* A volatile field resolves to a cosmokit *cell*, not to its value: the loader
|
|
51
|
+
* writes an edit into that same cell in place and emits `loader/volatile-update`
|
|
52
|
+
* instead of remounting the plugin. Read every value through `readConfig`
|
|
53
|
+
* (lib/config.js), and treat the plugin's config object as a live view.
|
|
54
|
+
*/
|
|
55
|
+
export const Config = z.object({
|
|
56
|
+
/** SSH connection timeout. */
|
|
57
|
+
connectTimeoutMs: z.number().step(1).min(1000).default(15000).volatile(),
|
|
58
|
+
/** Default timeout of one command; a tool argument may override it. */
|
|
59
|
+
commandTimeoutMs: z.number().step(1).min(1000).default(60000).volatile(),
|
|
60
|
+
/** A synchronous call longer than this becomes a ctx.jobs job. */
|
|
61
|
+
syncTimeoutMs: z.number().step(1).min(1000).default(20000).volatile(),
|
|
62
|
+
/** Per-stream output ceiling, applied to stdout and to stderr each; beyond it that stream is marked TRUNCATED. */
|
|
63
|
+
maxOutputChars: z.number().step(1).min(1024).default(200000).volatile(),
|
|
64
|
+
/** Idle timeout after which the pool closes a connection. */
|
|
65
|
+
idleTimeoutMs: z.number().step(1).min(1000).default(600000).volatile(),
|
|
66
|
+
/** Whether to append a line to audit.log for every srv_exec. */
|
|
67
|
+
auditEnabled: z.boolean().default(true).volatile(),
|
|
68
|
+
/** Rotate the audit log once it exceeds this size; 0 disables rotation. */
|
|
69
|
+
auditMaxBytes: z.number().step(1).min(0).default(0).volatile(),
|
|
70
|
+
/** Lifetime of a pending (two-step) dangerous operation. */
|
|
71
|
+
pendingTtlHours: z.number().step(1).min(1).default(24).volatile(),
|
|
72
|
+
/** How many servers the system prompt lists before collapsing the rest. */
|
|
73
|
+
maxListedServers: z.number().step(1).min(1).default(8).volatile(),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export { CONFIG_DEFAULTS, CONFIG_KEYS, readConfig, unbox } from './config.js'
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The resolved configuration shape (doc §10) — plain values, as {@link readConfig}
|
|
80
|
+
* returns them.
|
|
81
|
+
* @typedef {object} PluginConfig
|
|
82
|
+
* @property {number} connectTimeoutMs
|
|
83
|
+
* @property {number} commandTimeoutMs
|
|
84
|
+
* @property {number} syncTimeoutMs
|
|
85
|
+
* @property {number} maxOutputChars
|
|
86
|
+
* @property {number} idleTimeoutMs
|
|
87
|
+
* @property {boolean} auditEnabled
|
|
88
|
+
* @property {number} auditMaxBytes
|
|
89
|
+
* @property {number} pendingTtlHours
|
|
90
|
+
* @property {number} maxListedServers
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The configuration object the loader hands `apply`.
|
|
95
|
+
*
|
|
96
|
+
* Every field is `.volatile()`, so schemastery resolves each one to a cosmokit
|
|
97
|
+
* cell rather than to its value — the type is therefore deliberately opaque, and
|
|
98
|
+
* {@link readConfig} is the only sanctioned reader.
|
|
99
|
+
* @typedef {Record<string, unknown>} RawConfig
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build the registry for these settings (doc §3.5).
|
|
104
|
+
*
|
|
105
|
+
* `file` exists for tests and for an embedding host that keeps the data
|
|
106
|
+
* directory somewhere else; production code passes nothing and gets
|
|
107
|
+
* `$DSH_HOME/server-connector/servers.json`.
|
|
108
|
+
* @param {RawConfig} [config] - unused today, accepted so every unit factory
|
|
109
|
+
* has the same `(config, options)` signature.
|
|
110
|
+
* @param {{file?: string, env?: NodeJS.ProcessEnv}} [options]
|
|
111
|
+
* @returns {Registry}
|
|
112
|
+
*/
|
|
113
|
+
export function buildRegistry(config, options = {}) {
|
|
114
|
+
void config
|
|
115
|
+
return new Registry({ file: options.file ?? defaultServersFile(options.env) })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build the audit log for these settings (doc §3.4, §6.2, §10).
|
|
120
|
+
* `auditEnabled: false` keeps the log object usable but makes `append()` a
|
|
121
|
+
* no-op, which is what the gate needs: the command still runs.
|
|
122
|
+
* @param {RawConfig} config - supplies `auditEnabled` and `auditMaxBytes`.
|
|
123
|
+
* @param {{file?: string, env?: NodeJS.ProcessEnv}} [options]
|
|
124
|
+
* @returns {AuditLog}
|
|
125
|
+
*/
|
|
126
|
+
export function buildAuditLog(config, options = {}) {
|
|
127
|
+
const values = readConfig(config)
|
|
128
|
+
return new AuditLog({
|
|
129
|
+
file: options.file ?? defaultAuditFile(options.env),
|
|
130
|
+
enabled: values.auditEnabled,
|
|
131
|
+
maxBytes: values.auditMaxBytes,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build the host fingerprint store for these settings (doc §3.2, §6.4).
|
|
137
|
+
* @param {RawConfig} [config] - unused today, accepted so every unit factory
|
|
138
|
+
* has the same `(config, options)` signature.
|
|
139
|
+
* @param {{file?: string, env?: NodeJS.ProcessEnv}} [options]
|
|
140
|
+
* @returns {KnownHosts}
|
|
141
|
+
*/
|
|
142
|
+
export function buildKnownHosts(config, options = {}) {
|
|
143
|
+
void config
|
|
144
|
+
return new KnownHosts({ file: options.file ?? defaultKnownHostsFile(options.env) })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build the SSH connection pool for these settings (doc §7, §10).
|
|
149
|
+
*
|
|
150
|
+
* `resolvePassword` is injected rather than looked up here: the pool must not
|
|
151
|
+
* depend on `ctx.credentials` (doc §9.4 makes that service optional, and the
|
|
152
|
+
* tools layer owns credential refs). When no resolver is passed, a password
|
|
153
|
+
* server reports `NO_CREDENTIAL` instead of dialling with an empty password.
|
|
154
|
+
* @param {RawConfig} config - supplies the timeouts and output ceiling.
|
|
155
|
+
* @param {{
|
|
156
|
+
* knownHosts: KnownHosts,
|
|
157
|
+
* resolvePassword?: (server: import('./registry.js').ServerRecord) => Promise<string|null>,
|
|
158
|
+
* logger?: {info: Function, warn: Function, error: Function} | null,
|
|
159
|
+
* }} options
|
|
160
|
+
* @returns {Pool}
|
|
161
|
+
*/
|
|
162
|
+
export function buildPool(config, options) {
|
|
163
|
+
const values = readConfig(config)
|
|
164
|
+
return new Pool({
|
|
165
|
+
knownHosts: options.knownHosts,
|
|
166
|
+
resolvePassword: options.resolvePassword,
|
|
167
|
+
logger: options.logger ?? null,
|
|
168
|
+
connectTimeoutMs: values.connectTimeoutMs,
|
|
169
|
+
commandTimeoutMs: values.commandTimeoutMs,
|
|
170
|
+
idleTimeoutMs: values.idleTimeoutMs,
|
|
171
|
+
maxOutputChars: values.maxOutputChars,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build the two-step confirmation queue for these settings (doc §3.3, §8.3).
|
|
177
|
+
* @param {RawConfig} config - supplies `pendingTtlHours`.
|
|
178
|
+
* @param {{file?: string, env?: NodeJS.ProcessEnv, now?: () => Date}} [options]
|
|
179
|
+
* @returns {PendingQueue}
|
|
180
|
+
*/
|
|
181
|
+
export function buildPendingQueue(config, options = {}) {
|
|
182
|
+
return new PendingQueue({
|
|
183
|
+
file: options.file ?? defaultPendingFile(options.env),
|
|
184
|
+
ttlHours: readConfig(config).pendingTtlHours,
|
|
185
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Mount the plugin.
|
|
191
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx - the plugin's cordis context.
|
|
192
|
+
* @param {RawConfig} config - the validated {@link Config} value (volatile cells).
|
|
193
|
+
* @returns {void}
|
|
194
|
+
*/
|
|
195
|
+
export function apply(ctx, config) {
|
|
196
|
+
const knownHosts = buildKnownHosts(config)
|
|
197
|
+
const registry = buildRegistry(config)
|
|
198
|
+
const audit = buildAuditLog(config)
|
|
199
|
+
const pending = buildPendingQueue(config)
|
|
200
|
+
const pool = buildPool(config, {
|
|
201
|
+
knownHosts,
|
|
202
|
+
logger: ctx.logger,
|
|
203
|
+
// `credentials` is an optional service (doc §9.4): without it only key and
|
|
204
|
+
// agent authentication work, which is exactly NO_CREDENTIAL's message.
|
|
205
|
+
resolvePassword: async (server) => {
|
|
206
|
+
if (server.auth.kind !== 'password') return null
|
|
207
|
+
const credentials = ctx.get('credentials')
|
|
208
|
+
if (credentials === undefined || credentials === null) return null
|
|
209
|
+
const resolved = await credentials.resolve?.(server.auth.credentialRef)
|
|
210
|
+
const value = typeof resolved === 'string' ? resolved : resolved?.value
|
|
211
|
+
return typeof value === 'string' && value !== '' ? value : null
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// Confirmed pending operations need the registry, the pool, the audit log and
|
|
216
|
+
// (for a deleted password server) the credentials service. The applier is
|
|
217
|
+
// injected so lib/pending.js stays free of those units (doc §2.2).
|
|
218
|
+
pending.setApplier(
|
|
219
|
+
createPendingApplier({
|
|
220
|
+
registry,
|
|
221
|
+
pool,
|
|
222
|
+
audit,
|
|
223
|
+
getCredentials: () => ctx.get('credentials'),
|
|
224
|
+
logger: ctx.logger,
|
|
225
|
+
}),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
// `approval` (doc §6.1) and `jobs` (doc §5.4) are optional services: without
|
|
229
|
+
// them a confirmation fails closed and every command runs synchronously.
|
|
230
|
+
const surface = new ToolSurface({
|
|
231
|
+
registry,
|
|
232
|
+
pool,
|
|
233
|
+
audit,
|
|
234
|
+
approval: ctx.get('approval') ?? null,
|
|
235
|
+
jobs: ctx.get('jobs') ?? null,
|
|
236
|
+
config,
|
|
237
|
+
logger: ctx.logger,
|
|
238
|
+
})
|
|
239
|
+
surface.register(ctx)
|
|
240
|
+
surface.registerSystemPrompt(ctx)
|
|
241
|
+
|
|
242
|
+
// The system-prompt provider is synchronous, so it serves this cache; refresh
|
|
243
|
+
// it at startup and after the registry changes.
|
|
244
|
+
const refresh = () => {
|
|
245
|
+
surface.refresh().catch((error) => {
|
|
246
|
+
ctx.logger.warn('dsh-server-connector: could not read servers.json: %s', String(error))
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
refresh()
|
|
250
|
+
|
|
251
|
+
// Two-step operations expire on their own (doc §8.3: "进程启动时与每次读写时
|
|
252
|
+
// 各检查一次"); the startup sweep reports what it discarded.
|
|
253
|
+
void pending
|
|
254
|
+
.expire()
|
|
255
|
+
.then((discarded) => {
|
|
256
|
+
if (discarded.length === 0) return
|
|
257
|
+
ctx.logger.info('dsh-server-connector: discarded %d expired pending operation(s)', discarded.length)
|
|
258
|
+
})
|
|
259
|
+
.catch((error) => {
|
|
260
|
+
ctx.logger.warn('dsh-server-connector: could not read pending.json: %s', String(error))
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// A pool holds live sockets and idle timers; the plugin row must release both
|
|
264
|
+
// when it is unloaded, disabled or replaced. `ctx.effect` is cordis's
|
|
265
|
+
// lifecycle hook (an 'error'-style dispose event does not exist).
|
|
266
|
+
ctx.effect(() => () => pool.dispose(), 'dsh-server-connector: ssh pool')
|
|
267
|
+
|
|
268
|
+
// A volatile settings edit is written into the config object the loader gave
|
|
269
|
+
// this row and then announced — the plugin is NOT remounted, and `apply` is
|
|
270
|
+
// not called again. So every unit that cached a value has to be handed the new
|
|
271
|
+
// ones here, or the settings page would save and change nothing. The units
|
|
272
|
+
// read their other values at call time already (see `readConfig`).
|
|
273
|
+
//
|
|
274
|
+
// The event is emitted by the loader (`_commitVolatile` in dsh-loader), not
|
|
275
|
+
// declared in this plugin's own event map, hence the cast.
|
|
276
|
+
const onVolatileUpdate = /** @type {(name: any, listener: () => void, label?: string) => void} */ (ctx.on.bind(ctx))
|
|
277
|
+
onVolatileUpdate('loader/volatile-update', () => {
|
|
278
|
+
try {
|
|
279
|
+
const values = readConfig(config)
|
|
280
|
+
pool.configure(values)
|
|
281
|
+
audit.configure({ enabled: values.auditEnabled, maxBytes: values.auditMaxBytes })
|
|
282
|
+
pending.configure(values.pendingTtlHours)
|
|
283
|
+
} catch (error) {
|
|
284
|
+
ctx.logger.warn('dsh-server-connector: applying a live settings change failed: %s', String(error))
|
|
285
|
+
}
|
|
286
|
+
}, 'dsh-server-connector: live settings')
|
|
287
|
+
|
|
288
|
+
// The Client half's JSON routes (doc §8.5). `webServer` is an optional service
|
|
289
|
+
// (§9.4): a headless composition keeps the tools and simply has no panel. The
|
|
290
|
+
// panel's own changes go through the same registry, so the system-prompt cache
|
|
291
|
+
// is refreshed from there or the model would keep seeing the old server list.
|
|
292
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
293
|
+
const { dispose } = registerRoutes(webCtx, {
|
|
294
|
+
registry,
|
|
295
|
+
pool,
|
|
296
|
+
audit,
|
|
297
|
+
pending,
|
|
298
|
+
getCredentials: () => webCtx.get('credentials'),
|
|
299
|
+
config,
|
|
300
|
+
onChange: async () => {
|
|
301
|
+
await surface.refresh()
|
|
302
|
+
},
|
|
303
|
+
logger: webCtx.logger,
|
|
304
|
+
})
|
|
305
|
+
webCtx.effect(() => dispose, 'dsh-server-connector: json routes')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
ctx.logger.info(
|
|
309
|
+
'dsh-server-connector: %d srv_* tools registered (approval=%s, jobs=%s, credentials=%s, routes=%s)',
|
|
310
|
+
TOOL_NAMES.length,
|
|
311
|
+
ctx.get('approval') === undefined ? 'unavailable' : 'available',
|
|
312
|
+
ctx.get('jobs') === undefined ? 'unavailable (synchronous only)' : 'available',
|
|
313
|
+
ctx.get('credentials') === undefined ? 'unavailable (key/agent auth only)' : 'available',
|
|
314
|
+
ctx.get('webServer') === undefined ? 'unavailable (no panel)' : 'available',
|
|
315
|
+
)
|
|
316
|
+
}
|