@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,184 @@
|
|
|
1
|
+
// make-launch-spec.mjs -- generate the ACP agent launch spec for THIS machine.
|
|
2
|
+
//
|
|
3
|
+
// We deliberately do NOT launch `dsh.cmd`. That shim is:
|
|
4
|
+
// set "ELECTRON_RUN_AS_NODE=1"
|
|
5
|
+
// "<install>\DeepSeek Harness.exe" --expose-internals "<install>\resources\app.asar\
|
|
6
|
+
// dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js" %*
|
|
7
|
+
// and we replicate it exactly with an argv ARRAY, which avoids every Windows quoting
|
|
8
|
+
// hazard: `cmd.exe /c` loses quotes through the PowerShell -> node hand-off, and Node
|
|
9
|
+
// refuses to spawn a `.cmd` without a shell (EINVAL).
|
|
10
|
+
//
|
|
11
|
+
// NOTE: the cli.js path lives INSIDE app.asar, so ordinary fs existence checks report
|
|
12
|
+
// false -- Electron can read it, the filesystem API cannot. We therefore only test the
|
|
13
|
+
// .exe and let the argv path be constructed.
|
|
14
|
+
//
|
|
15
|
+
// Usage: node make-launch-spec.mjs [--install <dir>] [--profile acp] [--out <file>]
|
|
16
|
+
// [--permission-mode <mode>] [--dsh-home <dir>]
|
|
17
|
+
// [--settings-file <file>]
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { dirname, join, resolve } from 'node:path'
|
|
20
|
+
|
|
21
|
+
import { checkPairing } from './permission-pairing.mjs'
|
|
22
|
+
|
|
23
|
+
const argv = process.argv.slice(2)
|
|
24
|
+
function arg(name, fallback) {
|
|
25
|
+
const i = argv.indexOf(`--${name}`)
|
|
26
|
+
return i >= 0 && argv[i + 1] !== undefined ? argv[i + 1] : fallback
|
|
27
|
+
}
|
|
28
|
+
const PROFILE = arg('profile', 'acp')
|
|
29
|
+
const OUT = arg('out', 'agent-spec.json')
|
|
30
|
+
// Permission mode is read by dsh-base's sandbox-policy/approval rows from the
|
|
31
|
+
// environment, so it must be set in the CHILD's env to make experiments deterministic.
|
|
32
|
+
// 'workspace-write' => sandbox denies out-of-workspace writes and approval policy is 'ask',
|
|
33
|
+
// which is exactly the path that produces session/request_permission.
|
|
34
|
+
//
|
|
35
|
+
// NO --permission-mode MEANS "USE THE MACHINE'S CONFIGURED MODE", not "leave it unset": a
|
|
36
|
+
// hand-managed relay and the plugin's settings page both write this one value, and letting the
|
|
37
|
+
// manual path default to nothing is how a spec can silently keep an old mode after the setting was
|
|
38
|
+
// changed. The flag still wins when given, so an explicit deployment stays reproducible.
|
|
39
|
+
const PERMISSION_FLAG = arg('permission-mode', '')
|
|
40
|
+
let PERMISSION_MODE = PERMISSION_FLAG
|
|
41
|
+
let PERMISSION_MODE_SOURCE = PERMISSION_FLAG === '' ? 'none' : '--permission-mode'
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// GATE: refuse a mode this worker's profile cannot compose (PITFALLS F34).
|
|
45
|
+
//
|
|
46
|
+
// This is the point where a spec is WRITTEN, so it is the point where a bad mode must be refused --
|
|
47
|
+
// whether it came from --permission-mode or from the settings file. The plugin also knows the mode,
|
|
48
|
+
// but arming fails at spec-build time; letting a typo through here would start a child whose
|
|
49
|
+
// dsh-base rejects the value at load.
|
|
50
|
+
//
|
|
51
|
+
// Blocks on a KNOWN-BAD combination, and only warns when the profile cannot be read: refusing on
|
|
52
|
+
// "unknown" would make a legitimate worker undeployable, while staying silent about "unknown" would
|
|
53
|
+
// let the operator read silence as approval. The two cases are therefore reported differently.
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
const DSH_HOME = arg('dsh-home', process.env.DSH_HOME ?? join(process.env.USERPROFILE ?? '', '.dsh'))
|
|
56
|
+
const SETTINGS_FILE = arg('settings-file', join(DSH_HOME, 'storages', 'dsh-plugin-remote-use', 'settings.json'))
|
|
57
|
+
// Read once: the mode and the model both live in this file, and reading it twice invites the two
|
|
58
|
+
// values coming from different generations of it.
|
|
59
|
+
let SETTINGS = {}
|
|
60
|
+
try {
|
|
61
|
+
SETTINGS = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8').replace(/^\uFEFF/, ''))
|
|
62
|
+
} catch {
|
|
63
|
+
// Missing settings file is the normal case for a hand-managed worker that never opened the
|
|
64
|
+
// plugin's page. A corrupt one is caught by the mode gate if it produced no usable value.
|
|
65
|
+
SETTINGS = {}
|
|
66
|
+
}
|
|
67
|
+
if (PERMISSION_MODE === '') {
|
|
68
|
+
const fromSettings = SETTINGS !== null && typeof SETTINGS === 'object' ? SETTINGS.workerSessionMode : undefined
|
|
69
|
+
if (typeof fromSettings === 'string' && fromSettings !== '') {
|
|
70
|
+
PERMISSION_MODE = fromSettings
|
|
71
|
+
PERMISSION_MODE_SOURCE = `settings: ${SETTINGS_FILE}`
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The worker model, as `provider/model`. Empty means "leave the profile's acp row alone".
|
|
76
|
+
*
|
|
77
|
+
* It is applied as an OVERLAY passed with `--patch`, not by editing the profile: the plugin does
|
|
78
|
+
* not own `cordis.patch.yml`. The overlay is applied after the profile layer, so it wins.
|
|
79
|
+
*/
|
|
80
|
+
const WORKER_MODEL = SETTINGS !== null && typeof SETTINGS === 'object' && typeof SETTINGS.workerModel === 'string'
|
|
81
|
+
? SETTINGS.workerModel.trim()
|
|
82
|
+
: ''
|
|
83
|
+
let modelPatch = ''
|
|
84
|
+
if (WORKER_MODEL !== '') {
|
|
85
|
+
const slash = WORKER_MODEL.indexOf('/')
|
|
86
|
+
const provider = slash > 0 ? WORKER_MODEL.slice(0, slash).trim() : ''
|
|
87
|
+
const model = slash > 0 ? WORKER_MODEL.slice(slash + 1).trim() : ''
|
|
88
|
+
if (provider === '' || model === '' || /[\r\n]/.test(WORKER_MODEL) || /\s/.test(model)) {
|
|
89
|
+
console.error(`[make-launch-spec] REFUSING: workerModel "${WORKER_MODEL}" is not "provider/model"`)
|
|
90
|
+
console.error('Nothing was written. Fix the setting, then run this again.')
|
|
91
|
+
process.exit(3)
|
|
92
|
+
}
|
|
93
|
+
modelPatch = join(DSH_HOME, 'storages', 'dsh-plugin-remote-use', 'acp-model-overlay.yml')
|
|
94
|
+
const overlay = [
|
|
95
|
+
'# --- dsh-plugin-remote-use managed: worker model (generated from this machine\'s "workerModel" setting) ---',
|
|
96
|
+
'- id: acp',
|
|
97
|
+
" name: '@deepseek-ai/dsh-acp'",
|
|
98
|
+
' config:',
|
|
99
|
+
` provider: ${JSON.stringify(provider)}`,
|
|
100
|
+
` model: ${JSON.stringify(model)}`,
|
|
101
|
+
'# --- end dsh-plugin-remote-use managed: worker model ---',
|
|
102
|
+
'',
|
|
103
|
+
].join('\n')
|
|
104
|
+
try {
|
|
105
|
+
mkdirSync(dirname(modelPatch), { recursive: true })
|
|
106
|
+
writeFileSync(modelPatch, overlay, 'utf8')
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(`[make-launch-spec] REFUSING: cannot write the model overlay ${modelPatch}: ${error.message}`)
|
|
109
|
+
console.error('Nothing was written. Fix DSH_HOME or permissions, then run this again.')
|
|
110
|
+
process.exit(3)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const PROFILE_PATH = join(DSH_HOME, 'profiles', PROFILE, 'cordis.patch.yml')
|
|
114
|
+
let profileText
|
|
115
|
+
try {
|
|
116
|
+
profileText = readFileSync(PROFILE_PATH, 'utf8')
|
|
117
|
+
} catch {
|
|
118
|
+
profileText = undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const pairing = checkPairing({ mode: PERMISSION_MODE, profileText, profilePath: PROFILE_PATH })
|
|
122
|
+
if (pairing.severity === 'block') {
|
|
123
|
+
console.error(`[make-launch-spec] REFUSING to generate this spec: --permission-mode ${PERMISSION_MODE}`)
|
|
124
|
+
console.error('')
|
|
125
|
+
console.error(pairing.reason)
|
|
126
|
+
console.error('')
|
|
127
|
+
console.error('Nothing was written. Fix the profile, then run this again.')
|
|
128
|
+
process.exit(3)
|
|
129
|
+
}
|
|
130
|
+
if (pairing.severity === 'warn') {
|
|
131
|
+
console.error(`[make-launch-spec] WARNING: ${pairing.reason}`)
|
|
132
|
+
if (profileText === undefined) {
|
|
133
|
+
// "Could not verify" is not "verified fine", and the operator still needs to know what to check
|
|
134
|
+
// by hand when this script cannot read the profile (an unusual DSH_HOME, for instance).
|
|
135
|
+
console.error(` expected the profile patch at ${PROFILE_PATH}`)
|
|
136
|
+
console.error(' if it pins the approval policy, apply slave/acp-profile-approval.patch.yml')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Hard-coded profile paths first: under a SYSTEM scheduled task LOCALAPPDATA points at
|
|
141
|
+
// C:\Windows\system32\config\systemprofile, so environment-derived candidates miss.
|
|
142
|
+
const CANDIDATES = [
|
|
143
|
+
arg('install', ''),
|
|
144
|
+
join(process.env.LOCALAPPDATA ?? '', 'Programs', 'DeepSeek Harness'),
|
|
145
|
+
'C:\\Program Files\\DeepSeek Harness',
|
|
146
|
+
].filter((p) => typeof p === 'string' && p !== '')
|
|
147
|
+
|
|
148
|
+
let installDir = null
|
|
149
|
+
for (const c of CANDIDATES) {
|
|
150
|
+
if (existsSync(join(c, 'DeepSeek Harness.exe'))) { installDir = c; break }
|
|
151
|
+
}
|
|
152
|
+
if (installDir === null) {
|
|
153
|
+
console.error('[make-launch-spec] could not find "DeepSeek Harness.exe". Tried:')
|
|
154
|
+
for (const c of CANDIDATES) console.error(' ' + c)
|
|
155
|
+
process.exit(2)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const exe = join(installDir, 'DeepSeek Harness.exe')
|
|
159
|
+
const cli = [
|
|
160
|
+
installDir, 'resources', 'app.asar', 'dsh', 'node_modules',
|
|
161
|
+
'@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js',
|
|
162
|
+
].join('\\')
|
|
163
|
+
|
|
164
|
+
const spec = {
|
|
165
|
+
command: exe,
|
|
166
|
+
// `--patch` is applied AFTER the profile layer: that is how the model setting wins without
|
|
167
|
+
// editing the operator's `cordis.patch.yml`.
|
|
168
|
+
args: ['--expose-internals', cli, '--profile', PROFILE, ...(modelPatch === '' ? [] : ['--patch', modelPatch])],
|
|
169
|
+
env: {
|
|
170
|
+
ELECTRON_RUN_AS_NODE: '1',
|
|
171
|
+
...(PERMISSION_MODE === '' ? {} : { DSH_PERMISSION_MODE: PERMISSION_MODE }),
|
|
172
|
+
},
|
|
173
|
+
_note: 'replicates dsh.cmd with an argv array; never launch the .cmd shim from Node on Windows',
|
|
174
|
+
_installDir: installDir,
|
|
175
|
+
_permissionMode: PERMISSION_MODE === '' ? '(inherited)' : PERMISSION_MODE,
|
|
176
|
+
_permissionModeSource: PERMISSION_MODE_SOURCE,
|
|
177
|
+
_workerModel: WORKER_MODEL === '' ? '(inherit profile)' : WORKER_MODEL,
|
|
178
|
+
_modelOverlay: modelPatch,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
writeFileSync(OUT, JSON.stringify(spec, null, 2), 'utf8')
|
|
182
|
+
console.log(`wrote ${resolve(OUT)}`)
|
|
183
|
+
console.log(`mode: ${spec._permissionMode}${PERMISSION_MODE_SOURCE === 'none' ? '' : ` (from ${PERMISSION_MODE_SOURCE})`}`)
|
|
184
|
+
console.log(JSON.stringify(spec, null, 2))
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing which local address to advertise, and which ones may be bound.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS ITS OWN MODULE: two callers need the same answer -- the relay, to put an address
|
|
5
|
+
* in a pairing string, and the plugin's arming path, to offer a list of interfaces and to
|
|
6
|
+
* refuse a bind that is not one of them. Two copies of "which address is the real LAN one"
|
|
7
|
+
* would drift, and the failure mode is a pairing string or a listener that is well-formed and
|
|
8
|
+
* unreachable.
|
|
9
|
+
*
|
|
10
|
+
* THE MEASUREMENT THAT SHAPED IT: a Windows machine with Hyper-V, WSL, Docker or VirtualBox
|
|
11
|
+
* installed has several non-internal IPv4 addresses, enumerated in no useful order. Taking the
|
|
12
|
+
* FIRST one produced `172.20.0.1` on the development host -- a Hyper-V switch no peer can
|
|
13
|
+
* reach. So candidates are scored rather than filtered, and interfaces whose names give them
|
|
14
|
+
* away as virtual are rejected outright.
|
|
15
|
+
*
|
|
16
|
+
* It is a heuristic, which is why an explicit override exists at every call site.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Interface names that are virtual adapters, with the switch that owns them.
|
|
21
|
+
* Rejected outright rather than down-ranked: an operator who wants one can still name it
|
|
22
|
+
* explicitly, but nothing should ever pick it by accident.
|
|
23
|
+
*/
|
|
24
|
+
const VIRTUAL_ADAPTER = /(hyper-v|vethernet|wsl|docker|vmware|virtualbox|vbox|loopback|tailscale|zerotier|tap|tun|npcap|bluetooth)/iu
|
|
25
|
+
|
|
26
|
+
/** `a.b.c.d` -> a number, or -1 when it is not a dotted quad. */
|
|
27
|
+
export function ipv4Value(address) {
|
|
28
|
+
const parts = String(address).split('.')
|
|
29
|
+
if (parts.length !== 4) return -1
|
|
30
|
+
let value = 0
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
if (!/^\d{1,3}$/u.test(part)) return -1
|
|
33
|
+
const n = Number(part)
|
|
34
|
+
if (n > 255) return -1
|
|
35
|
+
value = (value * 256) + n
|
|
36
|
+
}
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True for the private ranges a LAN peer can normally reach. */
|
|
41
|
+
export function isPrivateIpv4(address) {
|
|
42
|
+
return address.startsWith('192.168.')
|
|
43
|
+
|| address.startsWith('10.')
|
|
44
|
+
|| /^172\.(1[6-9]|2\d|3[01])\./u.test(address)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* How good an address is to advertise or bind. Higher wins; `-1` means never choose it.
|
|
49
|
+
*
|
|
50
|
+
* Private ranges beat anything else because a peer on the LAN can reach them, and the classic
|
|
51
|
+
* `192.168.0.0/16` beats `10/8` beats the `172.16/12` block -- where container and switch
|
|
52
|
+
* addresses usually land. Link-local (`169.254.`) scores lowest but is not rejected: it is a
|
|
53
|
+
* real answer when nothing else is configured, and saying so beats offering nothing.
|
|
54
|
+
*/
|
|
55
|
+
export function addressScore(address, name) {
|
|
56
|
+
if (VIRTUAL_ADAPTER.test(String(name))) return -1
|
|
57
|
+
const value = ipv4Value(address)
|
|
58
|
+
if (value < 0) return -1
|
|
59
|
+
if (address === '127.0.0.1' || address.startsWith('127.')) return -1
|
|
60
|
+
if (address.startsWith('192.168.')) return 40
|
|
61
|
+
if (address.startsWith('10.')) return 30
|
|
62
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./u.test(address)) return 20
|
|
63
|
+
if (address.startsWith('169.254.')) return 1
|
|
64
|
+
return 10
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Every IPv4 address this machine could plausibly be reached on.
|
|
69
|
+
*
|
|
70
|
+
* @param {Record<string, Array<{address: string, family: string, internal: boolean}>>} interfaces
|
|
71
|
+
* as `os.networkInterfaces()` returns it -- injected so this is testable off-machine
|
|
72
|
+
* @returns {Array<{ address: string, name: string, score: number, private: boolean, virtual: boolean }>}
|
|
73
|
+
* best first; virtual adapters are included but scored -1, so a UI can explain why
|
|
74
|
+
* they are not offered
|
|
75
|
+
*/
|
|
76
|
+
export function candidateAddresses(interfaces) {
|
|
77
|
+
const out = []
|
|
78
|
+
for (const [name, list] of Object.entries(interfaces ?? {})) {
|
|
79
|
+
for (const info of list ?? []) {
|
|
80
|
+
if (info?.family !== 'IPv4' || info.internal === true) continue
|
|
81
|
+
const address = String(info.address ?? '')
|
|
82
|
+
// A malformed entry is not an address at all, so it is dropped. A VIRTUAL adapter is a
|
|
83
|
+
// real address that merely should not be chosen by default, so it stays in the list with
|
|
84
|
+
// a negative score -- that is what lets a UI say why it is not offered.
|
|
85
|
+
if (ipv4Value(address) < 0) continue
|
|
86
|
+
const virtual = VIRTUAL_ADAPTER.test(name)
|
|
87
|
+
out.push({
|
|
88
|
+
address,
|
|
89
|
+
name,
|
|
90
|
+
score: addressScore(address, name),
|
|
91
|
+
private: isPrivateIpv4(address),
|
|
92
|
+
virtual,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Best score first, then the lower address, so the order is stable across calls.
|
|
97
|
+
return out.sort((a, b) => (b.score - a.score) || (ipv4Value(a.address) - ipv4Value(b.address)))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The best address to advertise, or '' when there is none.
|
|
102
|
+
* @param {Record<string, any>} interfaces
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
export function bestAddress(interfaces) {
|
|
106
|
+
const [first] = candidateAddresses(interfaces)
|
|
107
|
+
return first === undefined || first.score <= 0 ? '' : first.address
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* May `address` be bound as the worker listener?
|
|
112
|
+
*
|
|
113
|
+
* `0.0.0.0` is REFUSED, not merely defaulted against. The decision was "bind a specific LAN
|
|
114
|
+
* interface, and do not offer a one-click all-interfaces option", and a rule that only lives in
|
|
115
|
+
* a dropdown is a rule the next caller ignores.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} address
|
|
118
|
+
* @param {Record<string, any>} interfaces
|
|
119
|
+
* @returns {{ ok: boolean, error: string }}
|
|
120
|
+
*/
|
|
121
|
+
export function assertBindable(address, interfaces) {
|
|
122
|
+
const value = String(address ?? '').trim()
|
|
123
|
+
if (value === '') return { ok: false, error: 'no bind address given' }
|
|
124
|
+
if (value === '0.0.0.0' || value === '::' || value === '*') {
|
|
125
|
+
return { ok: false, error: 'binding every interface is not offered; name the LAN address to listen on' }
|
|
126
|
+
}
|
|
127
|
+
if (value === '127.0.0.1' || value.startsWith('127.')) {
|
|
128
|
+
// Allowed, but only as an explicit choice: a loopback listener cannot be reached by a peer,
|
|
129
|
+
// so it is a test mode rather than a pairing.
|
|
130
|
+
return { ok: true, error: '' }
|
|
131
|
+
}
|
|
132
|
+
if (ipv4Value(value) < 0) return { ok: false, error: `not an IPv4 address: ${value}` }
|
|
133
|
+
const known = candidateAddresses(interfaces).find((c) => c.address === value)
|
|
134
|
+
if (known === undefined) {
|
|
135
|
+
return { ok: false, error: `${value} is not an address of this machine (choose one of the listed interfaces)` }
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, error: '' }
|
|
138
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-remote-use-slave",
|
|
3
|
+
"lockfileVersion": 3,
|
|
4
|
+
"requires": true,
|
|
5
|
+
"packages": {
|
|
6
|
+
"node_modules/ws": {
|
|
7
|
+
"version": "8.22.0",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=10.0.0"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"bufferutil": "^4.0.1",
|
|
14
|
+
"utf-8-validate": ">=5.0.2"
|
|
15
|
+
},
|
|
16
|
+
"peerDependenciesMeta": {
|
|
17
|
+
"bufferutil": {
|
|
18
|
+
"optional": true
|
|
19
|
+
},
|
|
20
|
+
"utf-8-validate": {
|
|
21
|
+
"optional": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
|
2
|
+
Copyright (c) 2013 Arnout Kazemier and contributors
|
|
3
|
+
Copyright (c) 2016 Luigi Pinca and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|