@hope_phenom/dsh-plugin-remote-use 0.0.0-stage → 0.2.24
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/LICENSE +21 -0
- package/README.md +207 -2
- package/README.zh.md +195 -0
- package/assets/icon.svg +47 -0
- package/cordis.patch.yml +41 -0
- package/lib/bridge/acp-bridge.js +905 -0
- package/lib/bridge/peer-connection.js +730 -0
- package/lib/client.js +2842 -0
- package/lib/core/acp-model-overlay.js +121 -0
- package/lib/core/approval-queue.js +216 -0
- package/lib/core/audit-log.js +322 -0
- package/lib/core/desktop-notify.js +173 -0
- package/lib/core/environment-prompt.js +76 -0
- package/lib/core/relay-supervisor.js +278 -0
- package/lib/core/review-engine.js +679 -0
- package/lib/core/sandbox-diagnosis.js +258 -0
- package/lib/core/sandbox-probe.js +310 -0
- package/lib/core/self-acp-spec.js +117 -0
- package/lib/core/selfcheck-runner.js +150 -0
- package/lib/core/settings-store.js +226 -0
- package/lib/core/shot-gc.js +334 -0
- package/lib/core/task-cwd.js +50 -0
- package/lib/core/task-memory.js +163 -0
- package/lib/core/task-registry.js +238 -0
- package/lib/core/toolcall-cache.js +206 -0
- package/lib/core/transfer-queue.js +58 -0
- package/lib/core/transfer.js +1144 -0
- package/lib/core/worker-arming.js +334 -0
- package/lib/index.js +2207 -0
- package/lib/provider/out-of-process-helpers.js +76 -0
- package/lib/provider/remote-provider.js +261 -0
- package/lib/tools/mediator-shim.mjs +180 -0
- package/lib/tools/prepare-sandbox-mediator.mjs +529 -0
- package/lib/tools/remote-tools.js +680 -0
- package/lib/tools/selfcheck-fix-prompt.md +206 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +72 -4
- package/slave/acp-profile-approval.patch.yml +66 -0
- package/slave/file-transfer.mjs +552 -0
- package/slave/integrity-probe.ps1 +99 -0
- package/slave/make-launch-spec.mjs +184 -0
- package/slave/net-address.mjs +138 -0
- package/slave/node_modules/.package-lock.json +26 -0
- package/slave/node_modules/ws/LICENSE +20 -0
- package/slave/node_modules/ws/README.md +548 -0
- package/slave/node_modules/ws/browser.js +8 -0
- package/slave/node_modules/ws/index.js +22 -0
- package/slave/node_modules/ws/lib/buffer-util.js +131 -0
- package/slave/node_modules/ws/lib/constants.js +19 -0
- package/slave/node_modules/ws/lib/event-target.js +292 -0
- package/slave/node_modules/ws/lib/extension.js +203 -0
- package/slave/node_modules/ws/lib/limiter.js +55 -0
- package/slave/node_modules/ws/lib/permessage-deflate.js +530 -0
- package/slave/node_modules/ws/lib/receiver.js +743 -0
- package/slave/node_modules/ws/lib/sender.js +607 -0
- package/slave/node_modules/ws/lib/stream.js +161 -0
- package/slave/node_modules/ws/lib/subprotocol.js +62 -0
- package/slave/node_modules/ws/lib/validation.js +152 -0
- package/slave/node_modules/ws/lib/websocket-server.js +562 -0
- package/slave/node_modules/ws/lib/websocket.js +1421 -0
- package/slave/node_modules/ws/package.json +74 -0
- package/slave/node_modules/ws/wrapper.mjs +21 -0
- package/slave/pairing.mjs +175 -0
- package/slave/permission-pairing.mjs +240 -0
- package/slave/prune-shots.mjs +170 -0
- package/slave/relay.mjs +1351 -0
- package/slave/repair-workspace-integrity.ps1 +75 -0
- package/slave/screenshot.ps1 +114 -0
- package/slave/selfcheck.ps1 +350 -0
- package/slave/stage-app.ps1 +116 -0
- package/slave/workspace-integrity.mjs +157 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arming this machine as a remote worker.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS OWNS, and what it deliberately does not. It owns the DECISION to accept remote
|
|
5
|
+
* execution, the credential, and the latch. It does not own the listener: `start`/`stop` are
|
|
6
|
+
* injected, so the state machine is complete and testable before a port is opened, and the
|
|
7
|
+
* listener cannot quietly acquire a policy of its own.
|
|
8
|
+
*
|
|
9
|
+
* THE SIX DECISIONS THIS ENCODES (§16.4):
|
|
10
|
+
* 1. LATCHED. Arming stays until it is explicitly disarmed. There is no timer that quietly
|
|
11
|
+
* stops accepting connections, because a remote party that is mid-task would experience
|
|
12
|
+
* that as a failure rather than as a policy.
|
|
13
|
+
* 2. A SPECIFIC LAN INTERFACE. `0.0.0.0` is refused, not merely discouraged -- see
|
|
14
|
+
* `slave/net-address.mjs`.
|
|
15
|
+
* 3. THE TOKEN IS SHOWN ONCE. `arm()` returns the plaintext exactly once; the status carries
|
|
16
|
+
* only a mask afterwards. It is persisted so a restart does not invalidate a pairing.
|
|
17
|
+
* 4. REFUSE WHEN THE SANDBOX DOES NOT CONTAIN. Not "warn and continue": a remote execution
|
|
18
|
+
* surface on an operator's workstation whose confinement is unverified is the thing this
|
|
19
|
+
* whole project exists to avoid.
|
|
20
|
+
* 5. LOCAL EMERGENCY STOP. `disarm()` never contacts the far end and never waits on it.
|
|
21
|
+
* 6. THE STATUS SAYS WHAT IS OPEN. Address, port, how long, and which credential is in use.
|
|
22
|
+
*
|
|
23
|
+
* WHY THE GATE RUNS THE PROBE SYNCHRONOUSLY AND EVERY TIME: the failure it looks for depends on
|
|
24
|
+
* the LAUNCH CONTEXT (a console-subsystem process allocates a console, a GUI-subsystem one does
|
|
25
|
+
* not), so a result cached from a previous run in a different context would be answering about
|
|
26
|
+
* a different machine.
|
|
27
|
+
*/
|
|
28
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
29
|
+
|
|
30
|
+
import { assertBindable, candidateAddresses } from '../../slave/net-address.mjs'
|
|
31
|
+
import { encodePairing } from '../../slave/pairing.mjs'
|
|
32
|
+
import { isAbsoluteTaskPath } from './task-cwd.js'
|
|
33
|
+
import { probeSandbox } from './sandbox-probe.js'
|
|
34
|
+
|
|
35
|
+
/** Port used when the operator does not name one. Matches the reference worker's relay. */
|
|
36
|
+
export const DEFAULT_ARM_PORT = 7331
|
|
37
|
+
|
|
38
|
+
/** How long a stored token hint is. Enough to recognise a credential, not to use one. */
|
|
39
|
+
const HINT_CHARS = 4
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} token
|
|
43
|
+
* @returns {string} a mask that reveals only the tail
|
|
44
|
+
*/
|
|
45
|
+
export function maskToken(token) {
|
|
46
|
+
const value = String(token ?? '')
|
|
47
|
+
if (value === '') return ''
|
|
48
|
+
return `\u2022`.repeat(12) + value.slice(-HINT_CHARS)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A fresh credential. 32 bytes of CSPRNG output, hex, so it survives YAML and JSON. */
|
|
52
|
+
export function generateToken() {
|
|
53
|
+
return randomBytes(32).toString('hex')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compare a presented token against the stored one without leaking its length or content
|
|
58
|
+
* through timing. Kept here rather than in the listener so the rule has one home.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} stored
|
|
61
|
+
* @param {string} presented
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
export function tokenMatches(stored, presented) {
|
|
65
|
+
if (typeof stored !== 'string' || stored === '' || typeof presented !== 'string' || presented === '') return false
|
|
66
|
+
// Hash both sides first: `timingSafeEqual` throws on differing lengths, and the length itself
|
|
67
|
+
// is information. Comparing digests makes every comparison the same length.
|
|
68
|
+
const a = createHash('sha256').update(stored).digest()
|
|
69
|
+
const b = createHash('sha256').update(presented).digest()
|
|
70
|
+
return timingSafeEqual(a, b)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class WorkerArming {
|
|
74
|
+
/**
|
|
75
|
+
* @param {object} deps
|
|
76
|
+
* @param {() => Record<string, any>} deps.interfaces `os.networkInterfaces()` in production
|
|
77
|
+
* @param {(state: object) => void} [deps.persist] write the armed state so it survives a restart
|
|
78
|
+
* @param {(bindAddress: string, port: number, token: string, taskCwd: string) => void} [deps.start]
|
|
79
|
+
* @param {() => void} [deps.stop]
|
|
80
|
+
* @param {() => { ok: boolean, reason: string, checks?: object[] }} [deps.probe] sandbox gate
|
|
81
|
+
* @param {() => number} [deps.now]
|
|
82
|
+
* @param {() => string} [deps.mint]
|
|
83
|
+
* @param {() => object | null} [deps.listenerStatus] the supervised listener's own status, if any
|
|
84
|
+
* @param {{ info?: Function, warn?: Function }} [deps.log]
|
|
85
|
+
* @param {object} [deps.stored] the persisted state, if any
|
|
86
|
+
*/
|
|
87
|
+
constructor(deps = {}) {
|
|
88
|
+
this.interfaces = deps.interfaces ?? (() => ({}))
|
|
89
|
+
this.persist = deps.persist ?? (() => {})
|
|
90
|
+
this.start = deps.start ?? (() => {})
|
|
91
|
+
this.stop = deps.stop ?? (() => {})
|
|
92
|
+
this.probe = deps.probe ?? (() => probeSandbox())
|
|
93
|
+
this.listenerStatus = deps.listenerStatus ?? (() => null)
|
|
94
|
+
this.now = deps.now ?? (() => Date.now())
|
|
95
|
+
this.mint = deps.mint ?? generateToken
|
|
96
|
+
const log = deps.log ?? {}
|
|
97
|
+
this.log = { info: log.info ?? (() => {}), warn: log.warn ?? (() => {}) }
|
|
98
|
+
|
|
99
|
+
const stored = deps.stored ?? {}
|
|
100
|
+
this.armed = false
|
|
101
|
+
this.bindAddress = ''
|
|
102
|
+
this.port = DEFAULT_ARM_PORT
|
|
103
|
+
this.token = ''
|
|
104
|
+
// The directory this machine's tasks run in. It goes into the pairing string because the
|
|
105
|
+
// WORKER is the only side that can know a path on the worker -- the master learns it by paste
|
|
106
|
+
// or not at all. Kept as issued: the master owns the stored copy from then on.
|
|
107
|
+
this.taskCwd = ''
|
|
108
|
+
this.armedAt = 0
|
|
109
|
+
this.lastError = ''
|
|
110
|
+
this.lastProbe = { ok: false, reason: 'not checked yet' }
|
|
111
|
+
|
|
112
|
+
// The token is restored, NOT regenerated: decision 3 persists it so a restart does not
|
|
113
|
+
// invalidate a pairing the operator has already distributed. Regenerating here would be the
|
|
114
|
+
// quiet way to break every paired master on reboot.
|
|
115
|
+
if (stored !== null && typeof stored === 'object' && typeof stored.token === 'string' && stored.token !== '') {
|
|
116
|
+
this.token = stored.token
|
|
117
|
+
}
|
|
118
|
+
if (typeof stored?.bindAddress === 'string') this.bindAddress = stored.bindAddress
|
|
119
|
+
if (Number.isFinite(stored?.port)) this.port = Number(stored.port)
|
|
120
|
+
if (typeof stored?.taskCwd === 'string') this.taskCwd = stored.taskCwd
|
|
121
|
+
if (stored?.armed === true && this.token !== '' && this.bindAddress !== '') {
|
|
122
|
+
// THE GATE IS RE-RUN ON RESTORE, not skipped because it passed once.
|
|
123
|
+
//
|
|
124
|
+
// The sandbox is not a property of this plugin -- it is the mediator wired into the acp
|
|
125
|
+
// profile, and the mediator is materialized OUT of the DSH bundle. A DSH upgrade therefore
|
|
126
|
+
// invalidates it (the shim would drive the previous version's runner), and so does deleting
|
|
127
|
+
// the directory. Without this check, a machine that was armed last week would silently go on
|
|
128
|
+
// accepting remote execution after its confinement broke -- which is the exact situation
|
|
129
|
+
// decision 4 exists to prevent.
|
|
130
|
+
//
|
|
131
|
+
// It costs a probe, and only when the stored state says armed: a disarmed machine (the
|
|
132
|
+
// default) pays nothing at boot.
|
|
133
|
+
this.lastProbe = this.probe()
|
|
134
|
+
if (this.lastProbe.ok !== true) {
|
|
135
|
+
this.armed = false
|
|
136
|
+
this.lastError = `refused to restore remote execution: the sandbox no longer contains this machine (${this.lastProbe.reason})`
|
|
137
|
+
this.log.warn(this.lastError)
|
|
138
|
+
} else {
|
|
139
|
+
this.armed = true
|
|
140
|
+
this.armedAt = Number.isFinite(stored.armedAt) ? Number(stored.armedAt) : this.now()
|
|
141
|
+
try {
|
|
142
|
+
this.start(this.bindAddress, this.port, this.token, this.taskCwd)
|
|
143
|
+
this.log.info(`worker role restored: listening on ${this.bindAddress}:${this.port}`)
|
|
144
|
+
} catch (error) {
|
|
145
|
+
// A failed rebind must NOT leave the object claiming to be armed: the operator would
|
|
146
|
+
// see "accepting remote execution" while nothing is listening.
|
|
147
|
+
this.armed = false
|
|
148
|
+
this.lastError = `could not restore the listener: ${error instanceof Error ? error.message : String(error)}`
|
|
149
|
+
this.log.warn(this.lastError)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** What the page shows. Never includes the token, only its mask. */
|
|
156
|
+
status() {
|
|
157
|
+
return {
|
|
158
|
+
armed: this.armed,
|
|
159
|
+
bindAddress: this.bindAddress,
|
|
160
|
+
port: this.port,
|
|
161
|
+
taskCwd: this.taskCwd,
|
|
162
|
+
tokenSet: this.token !== '',
|
|
163
|
+
tokenHint: maskToken(this.token),
|
|
164
|
+
armedAt: this.armedAt,
|
|
165
|
+
uptimeMs: this.armed && this.armedAt > 0 ? Math.max(0, this.now() - this.armedAt) : 0,
|
|
166
|
+
lastError: this.lastError,
|
|
167
|
+
// Every interface, including the ones that must not be chosen, so the page can explain
|
|
168
|
+
// rather than silently omit.
|
|
169
|
+
interfaces: candidateAddresses(this.interfaces()),
|
|
170
|
+
probe: { ok: this.lastProbe.ok, reason: this.lastProbe.reason },
|
|
171
|
+
// The listener's own view, including the session mode its launch spec carries. The arming
|
|
172
|
+
// object owns the decision to accept remote work; it does not own what the relay was started
|
|
173
|
+
// with, and merging the two here is what lets the page show BOTH instead of assuming they agree.
|
|
174
|
+
listener: this.listenerStatus(),
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Can this machine be armed at all, and is the requested address usable?
|
|
180
|
+
*
|
|
181
|
+
* Runs the sandbox probe EVERY time: the answer depends on the launch context, so a cached
|
|
182
|
+
* one is an answer about a different context.
|
|
183
|
+
*
|
|
184
|
+
* @param {{ bindAddress?: string, port?: number, taskCwd?: string }} [request]
|
|
185
|
+
* @returns {{ ok: boolean, blockers: string[], probe: object }}
|
|
186
|
+
*/
|
|
187
|
+
preflight(request = {}) {
|
|
188
|
+
const blockers = []
|
|
189
|
+
this.lastProbe = this.probe()
|
|
190
|
+
if (this.lastProbe.ok !== true) {
|
|
191
|
+
blockers.push(`the sandbox does not contain this machine: ${this.lastProbe.reason}`)
|
|
192
|
+
}
|
|
193
|
+
const address = request.bindAddress ?? this.bindAddress
|
|
194
|
+
const bindable = assertBindable(address, this.interfaces())
|
|
195
|
+
if (!bindable.ok) blockers.push(bindable.error)
|
|
196
|
+
const port = Number(request.port ?? this.port ?? DEFAULT_ARM_PORT)
|
|
197
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) blockers.push(`port must be 1..65535, got ${port}`)
|
|
198
|
+
// A BLANK task directory is allowed -- the master can still be told one separately -- but a
|
|
199
|
+
// relative one is the same class of error as an unbindable address, and worse: it would go
|
|
200
|
+
// into the pairing string as if it were usable, and the master would have no reason to doubt
|
|
201
|
+
// it. Caught here rather than at the first delegation on the far machine.
|
|
202
|
+
const taskCwd = String(request.taskCwd ?? this.taskCwd).trim()
|
|
203
|
+
if (taskCwd !== '' && !isAbsoluteTaskPath(taskCwd)) {
|
|
204
|
+
blockers.push(`the task directory must be an absolute path on this machine, got "${taskCwd}"`)
|
|
205
|
+
}
|
|
206
|
+
return { ok: blockers.length === 0, blockers, probe: this.lastProbe }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Accept remote execution. Returns the plaintext token EXACTLY ONCE.
|
|
211
|
+
*
|
|
212
|
+
* @param {{ bindAddress: string, port?: number, taskCwd?: string }} request
|
|
213
|
+
* @returns {{ ok: boolean, token?: string, error?: string, blockers?: string[] }}
|
|
214
|
+
*/
|
|
215
|
+
arm(request) {
|
|
216
|
+
if (this.armed) {
|
|
217
|
+
// Not an error worth hiding: the page may be showing a stale state, and re-minting a token
|
|
218
|
+
// here would break a pairing the operator has already distributed.
|
|
219
|
+
return { ok: false, error: `already armed on ${this.bindAddress}:${this.port}` }
|
|
220
|
+
}
|
|
221
|
+
const pre = this.preflight(request)
|
|
222
|
+
if (!pre.ok) return { ok: false, error: pre.blockers[0], blockers: pre.blockers }
|
|
223
|
+
|
|
224
|
+
const bindAddress = String(request.bindAddress).trim()
|
|
225
|
+
const port = Number(request.port ?? DEFAULT_ARM_PORT)
|
|
226
|
+
const taskCwd = String(request.taskCwd ?? '').trim()
|
|
227
|
+
|
|
228
|
+
// A NEW credential every time it is armed, because decision 3 is "generated WHEN ENABLED".
|
|
229
|
+
//
|
|
230
|
+
// The persisted one exists for a different event: a RESTART. Arming is the operator saying
|
|
231
|
+
// "accept remote execution now", and a credential that survived an explicit stop is one they
|
|
232
|
+
// have no reason to believe is still valid. The distinction matters because the two cases look
|
|
233
|
+
// identical in the settings file -- `armed` is false and a token is present either way -- so
|
|
234
|
+
// only the arm path can tell them apart.
|
|
235
|
+
//
|
|
236
|
+
// Caught by reading the code against its own comment: the comment said this, the line did
|
|
237
|
+
// `this.token === '' ? this.mint() : this.token`, and a disarm-then-arm left the old pairing
|
|
238
|
+
// working. A test now asserts the rotation.
|
|
239
|
+
const token = this.mint()
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
this.start(bindAddress, port, token, taskCwd)
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
245
|
+
this.lastError = `could not start the listener: ${message}`
|
|
246
|
+
return { ok: false, error: this.lastError }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.armed = true
|
|
250
|
+
this.bindAddress = bindAddress
|
|
251
|
+
this.port = port
|
|
252
|
+
this.taskCwd = taskCwd
|
|
253
|
+
this.token = token
|
|
254
|
+
this.armedAt = this.now()
|
|
255
|
+
this.lastError = ''
|
|
256
|
+
this.#persist()
|
|
257
|
+
this.log.info(`worker role armed on ${bindAddress}:${port}`)
|
|
258
|
+
// Both forms, ONCE. The caller shows them and keeps neither; every later read of the status
|
|
259
|
+
// carries only the mask.
|
|
260
|
+
return { ok: true, token, pairing: this.pairingString() }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Stop accepting remote execution. Decision 5: local, immediate, and never dependent on the
|
|
265
|
+
* far end -- this is the switch that must work when the remote party is misbehaving.
|
|
266
|
+
*
|
|
267
|
+
* The token is KEPT in storage so a restart restores the same pairing; it is cleared in
|
|
268
|
+
* memory so the running process cannot accept it again.
|
|
269
|
+
*
|
|
270
|
+
* @param {{ forget?: boolean }} [opts] `forget` also invalidates the credential
|
|
271
|
+
*/
|
|
272
|
+
disarm(opts = {}) {
|
|
273
|
+
const wasArmed = this.armed
|
|
274
|
+
this.armed = false
|
|
275
|
+
this.armedAt = 0
|
|
276
|
+
try {
|
|
277
|
+
this.stop()
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Report but do not re-arm: a listener that failed to stop must not leave the object
|
|
280
|
+
// claiming to be safe, and the operator needs to know which it is.
|
|
281
|
+
this.lastError = `the listener did not stop cleanly: ${error instanceof Error ? error.message : String(error)}`
|
|
282
|
+
this.log.warn(this.lastError)
|
|
283
|
+
}
|
|
284
|
+
if (opts.forget === true) this.token = ''
|
|
285
|
+
this.#persist()
|
|
286
|
+
if (wasArmed) this.log.info(`worker role disarmed (${opts.forget === true ? 'credential discarded' : 'credential kept'})`)
|
|
287
|
+
return { ok: this.lastError === '', wasArmed }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The pairing string for the credential in force, or '' when disarmed.
|
|
292
|
+
*
|
|
293
|
+
* Returned alongside the token at arm time and never again. The string carries the credential
|
|
294
|
+
* in recoverable form, so treating it as re-readable would undo decision 3 -- the point of
|
|
295
|
+
* "shown once" is that the secret is not sitting in a page that can be screenshotted or left
|
|
296
|
+
* open. Re-issuing is a deliberate act: `disarm({ forget: true })` then `arm()`.
|
|
297
|
+
*
|
|
298
|
+
* It carries the TASK DIRECTORY too. That is not decoration: the master cannot guess a path on
|
|
299
|
+
* this machine, and a worker paired without one cannot accept a task at all. Leaving it out is
|
|
300
|
+
* how pairing produced a worker that failed on its first delegation instead of at the paste
|
|
301
|
+
* (PITFALLS F16). `encodePairing` omits the field when it is empty, so a worker that has no
|
|
302
|
+
* opinion still produces a valid string -- the master must then be told one.
|
|
303
|
+
*/
|
|
304
|
+
pairingString() {
|
|
305
|
+
if (!this.armed || this.token === '') return ''
|
|
306
|
+
return encodePairing({
|
|
307
|
+
url: `ws://${this.bindAddress}:${this.port}`,
|
|
308
|
+
token: this.token,
|
|
309
|
+
cwd: this.taskCwd,
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Whether a presented credential is the one in force. */
|
|
314
|
+
accepts(presented) {
|
|
315
|
+
if (!this.armed) return false
|
|
316
|
+
return tokenMatches(this.token, presented)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#persist() {
|
|
320
|
+
try {
|
|
321
|
+
this.persist({
|
|
322
|
+
armed: this.armed,
|
|
323
|
+
bindAddress: this.bindAddress,
|
|
324
|
+
port: this.port,
|
|
325
|
+
taskCwd: this.taskCwd,
|
|
326
|
+
token: this.token,
|
|
327
|
+
armedAt: this.armedAt,
|
|
328
|
+
})
|
|
329
|
+
} catch (error) {
|
|
330
|
+
this.lastError = `could not persist the worker role: ${error instanceof Error ? error.message : String(error)}`
|
|
331
|
+
this.log.warn(this.lastError)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|