dsh-mesh-node 0.0.0-stage → 0.0.2
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 +8 -2
- package/package.json +43 -4
- package/src/cli.js +1246 -0
- package/src/core/acl.js +81 -0
- package/src/core/audit.js +75 -0
- package/src/core/autostart.js +119 -0
- package/src/core/cluster.js +341 -0
- package/src/core/config.js +622 -0
- package/src/core/desktop-proxy.js +85 -0
- package/src/core/ds-web.js +394 -0
- package/src/core/host-agent.js +263 -0
- package/src/core/jobs.js +259 -0
- package/src/core/pairing.js +141 -0
- package/src/core/peer-client.js +273 -0
- package/src/core/peer-doctor.js +294 -0
- package/src/core/peer-http.js +61 -0
- package/src/core/repair.js +93 -0
- package/src/core/restart-policy.js +21 -0
- package/src/core/result.js +29 -0
- package/src/core/sakura-api.js +103 -0
- package/src/core/session-profile.js +146 -0
- package/src/core/tls.js +160 -0
- package/src/core/tunnels.js +212 -0
- package/src/core/util.js +68 -0
- package/src/e2e-autostart.js +58 -0
- package/src/e2e-channel.js +384 -0
- package/src/e2e-cluster.js +417 -0
- package/src/e2e-host.js +411 -0
- package/src/e2e-onboard.js +232 -0
- package/src/e2e-pair.js +317 -0
- package/src/e2e-plugin.js +395 -0
- package/src/e2e-repair.js +297 -0
- package/src/index.js +20 -0
- package/src/peer-bridge.js +124 -0
- package/src/selftest.js +872 -0
- package/src/server.js +317 -0
- package/src/tools/alerts.js +80 -0
- package/src/tools/channel.js +253 -0
- package/src/tools/cluster.js +235 -0
- package/src/tools/config-patch.js +182 -0
- package/src/tools/control-apps.js +469 -0
- package/src/tools/control-path.js +160 -0
- package/src/tools/control.js +232 -0
- package/src/tools/core.js +79 -0
- package/src/tools/delegate.js +653 -0
- package/src/tools/fs.js +436 -0
- package/src/tools/git.js +266 -0
- package/src/tools/host.js +305 -0
- package/src/tools/index.js +122 -0
- package/src/tools/repair-fs.js +130 -0
- package/src/tools/repair.js +317 -0
- package/src/tools/session-history.js +60 -0
- package/src/tools/shell.js +66 -0
- package/src/tools/tasks.js +117 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { hostname } from 'node:os'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { ok, denied, invalidArg } from '../core/result.js'
|
|
5
|
+
import { isProxyTimeout, maybeProxy, proxyTimeoutMessage } from '../core/desktop-proxy.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* pc_action:确定性电脑操作(白名单枚举,不是远程 shell / 远程键盘)。
|
|
9
|
+
*
|
|
10
|
+
* 设计要点:
|
|
11
|
+
* - 只接受枚举动作,未知动作直接拒绝;
|
|
12
|
+
* - Windows 用系统自带命令/PowerShell COM/虚拟按键实现;Linux/macOS 提供有限实现;
|
|
13
|
+
* - shutdown/restart 必须 confirm=true;sleep 也建议 confirm;
|
|
14
|
+
* - 按 tokenId 限频(默认 60 次/分钟),防脚本风暴;
|
|
15
|
+
* - 不接收任意程序路径、任意按键串、任意命令行。
|
|
16
|
+
*/
|
|
17
|
+
const ACTIONS = [
|
|
18
|
+
'info',
|
|
19
|
+
'lock',
|
|
20
|
+
'show-desktop',
|
|
21
|
+
'task-manager',
|
|
22
|
+
'mute',
|
|
23
|
+
'volume-up',
|
|
24
|
+
'volume-down',
|
|
25
|
+
'media-play-pause',
|
|
26
|
+
'media-next',
|
|
27
|
+
'media-prev',
|
|
28
|
+
'sleep',
|
|
29
|
+
'cancel-shutdown',
|
|
30
|
+
'shutdown',
|
|
31
|
+
'restart'
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const CONFIRM_REQUIRED = new Set(['sleep', 'shutdown', 'restart'])
|
|
35
|
+
|
|
36
|
+
const WINDOWS = process.platform === 'win32'
|
|
37
|
+
const DARWIN = process.platform === 'darwin'
|
|
38
|
+
const LINUX = process.platform === 'linux'
|
|
39
|
+
|
|
40
|
+
const VK = {
|
|
41
|
+
mute: 0xad,
|
|
42
|
+
volumeDown: 0xae,
|
|
43
|
+
volumeUp: 0xaf,
|
|
44
|
+
mediaNext: 0xb0,
|
|
45
|
+
mediaPrev: 0xb1,
|
|
46
|
+
mediaPlayPause: 0xb3
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function run(command, args, timeoutMs = 15000) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const started = Date.now()
|
|
52
|
+
let stdout = ''
|
|
53
|
+
let stderr = ''
|
|
54
|
+
let child
|
|
55
|
+
try {
|
|
56
|
+
child = spawn(command, args, { windowsHide: true, timeout: timeoutMs })
|
|
57
|
+
} catch (error) {
|
|
58
|
+
resolve({ ok: false, code: null, error: String(error?.message || error), stdout, stderr, ms: Date.now() - started })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
child.stdout?.on('data', (chunk) => (stdout += chunk))
|
|
62
|
+
child.stderr?.on('data', (chunk) => (stderr += chunk))
|
|
63
|
+
child.on('error', (error) => {
|
|
64
|
+
resolve({ ok: false, code: null, error: String(error?.message || error), stdout, stderr, ms: Date.now() - started })
|
|
65
|
+
})
|
|
66
|
+
child.on('close', (code) => {
|
|
67
|
+
resolve({ ok: code === 0, code, error: code === 0 ? null : stderr.trim() || `exit ${code}`, stdout: stdout.trim().slice(0, 4000), stderr: stderr.trim().slice(0, 4000), ms: Date.now() - started })
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function powershell(script) {
|
|
73
|
+
return run('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function keybdEvent(vk) {
|
|
77
|
+
const script = `Add-Type -Namespace Win32 -Name Keyboard -MemberDefinition '[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);'; [Win32.Keyboard]::keybd_event(${vk}, 0, 0, [UIntPtr]::Zero); Start-Sleep -Milliseconds 60; [Win32.Keyboard]::keybd_event(${vk}, 0, 2, [UIntPtr]::Zero)`
|
|
78
|
+
return powershell(script)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function executeWindows(action) {
|
|
82
|
+
switch (action) {
|
|
83
|
+
case 'lock':
|
|
84
|
+
return run('rundll32.exe', ['user32.dll,LockWorkStation'])
|
|
85
|
+
case 'show-desktop':
|
|
86
|
+
return powershell('(New-Object -ComObject Shell.Application).ToggleDesktop()')
|
|
87
|
+
case 'task-manager': {
|
|
88
|
+
try {
|
|
89
|
+
const child = spawn('taskmgr.exe', [], { detached: true, stdio: 'ignore', windowsHide: false })
|
|
90
|
+
child.unref()
|
|
91
|
+
return { ok: true, code: 0, detail: 'taskmgr.exe launched', ms: 0, stdout: '', stderr: '' }
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return { ok: false, error: String(error?.message || error), ms: 0 }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
case 'mute':
|
|
97
|
+
return keybdEvent(VK.mute)
|
|
98
|
+
case 'volume-up':
|
|
99
|
+
return keybdEvent(VK.volumeUp)
|
|
100
|
+
case 'volume-down':
|
|
101
|
+
return keybdEvent(VK.volumeDown)
|
|
102
|
+
case 'media-play-pause':
|
|
103
|
+
return keybdEvent(VK.mediaPlayPause)
|
|
104
|
+
case 'media-next':
|
|
105
|
+
return keybdEvent(VK.mediaNext)
|
|
106
|
+
case 'media-prev':
|
|
107
|
+
return keybdEvent(VK.mediaPrev)
|
|
108
|
+
case 'sleep':
|
|
109
|
+
return run('rundll32.exe', ['powrprof.dll,SetSuspendState', '0,1,0'])
|
|
110
|
+
case 'cancel-shutdown':
|
|
111
|
+
return run('shutdown.exe', ['/a'])
|
|
112
|
+
case 'shutdown':
|
|
113
|
+
return run('shutdown.exe', ['/s', '/t', '15'])
|
|
114
|
+
case 'restart':
|
|
115
|
+
return run('shutdown.exe', ['/r', '/t', '15'])
|
|
116
|
+
default:
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function executeLinux(action) {
|
|
122
|
+
switch (action) {
|
|
123
|
+
case 'lock':
|
|
124
|
+
return run('loginctl', ['lock-session'])
|
|
125
|
+
case 'show-desktop': {
|
|
126
|
+
// 优先 xdotool(未装则失败,不安装任何东西)
|
|
127
|
+
return run('xdotool', ['key', 'super+d'])
|
|
128
|
+
}
|
|
129
|
+
case 'sleep':
|
|
130
|
+
return run('systemctl', ['suspend'])
|
|
131
|
+
case 'shutdown':
|
|
132
|
+
return run('shutdown', ['-h', '+1'])
|
|
133
|
+
case 'restart':
|
|
134
|
+
return run('shutdown', ['-r', '+1'])
|
|
135
|
+
case 'info':
|
|
136
|
+
return { ok: true, code: 0, detail: 'linux', ms: 0 }
|
|
137
|
+
default:
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function executeDarwin(action) {
|
|
143
|
+
switch (action) {
|
|
144
|
+
case 'lock':
|
|
145
|
+
return run('pmset', ['displaysleepnow'])
|
|
146
|
+
case 'sleep':
|
|
147
|
+
return run('pmset', ['sleepnow'])
|
|
148
|
+
case 'shutdown':
|
|
149
|
+
return run('shutdown', ['-h', '+1'])
|
|
150
|
+
case 'restart':
|
|
151
|
+
return run('shutdown', ['-r', '+1'])
|
|
152
|
+
default:
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function buildControl(ctx) {
|
|
158
|
+
const rate = new Map()
|
|
159
|
+
const RATE_WINDOW_MS = 60_000
|
|
160
|
+
const RATE_MAX = 60
|
|
161
|
+
|
|
162
|
+
function rateLimited(peerId) {
|
|
163
|
+
const now = Date.now()
|
|
164
|
+
const key = String(peerId || 'local')
|
|
165
|
+
const list = (rate.get(key) || []).filter((t) => now - t < RATE_WINDOW_MS)
|
|
166
|
+
if (list.length >= RATE_MAX) return true
|
|
167
|
+
list.push(now)
|
|
168
|
+
rate.set(key, list)
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
name: 'pc_action',
|
|
175
|
+
title: 'Run a whitelisted desktop action on the peer machine',
|
|
176
|
+
description:
|
|
177
|
+
'Deterministic desktop actions on the peer machine (NOT a remote shell/keyboard). Whitelisted actions: ' +
|
|
178
|
+
ACTIONS.join(', ') +
|
|
179
|
+
'. shutdown/restart/sleep require confirm=true. Actions needing an interactive logged-in desktop (show-desktop/media keys/volume) may fail while the machine is locked.',
|
|
180
|
+
inputSchema: z.object({
|
|
181
|
+
action: z.enum(ACTIONS),
|
|
182
|
+
confirm: z.boolean().optional()
|
|
183
|
+
}),
|
|
184
|
+
handler: async ({ action, confirm }, { peerId } = {}) => {
|
|
185
|
+
const config = ctx.getConfig()
|
|
186
|
+
if (config?.pcControl?.enabled !== true) {
|
|
187
|
+
return denied('pc_action is disabled on this node (config.pcControl.enabled=false)')
|
|
188
|
+
}
|
|
189
|
+
if (rateLimited(peerId)) {
|
|
190
|
+
return denied('rate limit exceeded (60 actions/minute per peer token)')
|
|
191
|
+
}
|
|
192
|
+
if (CONFIRM_REQUIRED.has(action) && confirm !== true) {
|
|
193
|
+
return invalidArg(`${action} requires confirm=true`)
|
|
194
|
+
}
|
|
195
|
+
if (action !== 'info') {
|
|
196
|
+
const proxied = await maybeProxy('pc_action', { action, confirm })
|
|
197
|
+
if (isProxyTimeout(proxied)) return invalidArg(proxyTimeoutMessage('pc_action', proxied))
|
|
198
|
+
if (proxied) return proxied
|
|
199
|
+
}
|
|
200
|
+
const started = Date.now()
|
|
201
|
+
if (action === 'info') {
|
|
202
|
+
return ok({
|
|
203
|
+
action,
|
|
204
|
+
host: hostname(),
|
|
205
|
+
platform: process.platform,
|
|
206
|
+
arch: process.arch,
|
|
207
|
+
uptimeSec: Math.floor(process.uptime()),
|
|
208
|
+
actions: ACTIONS,
|
|
209
|
+
confirmRequired: [...CONFIRM_REQUIRED],
|
|
210
|
+
ms: Date.now() - started
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
const result = WINDOWS ? await executeWindows(action) : DARWIN ? await executeDarwin(action) : LINUX ? await executeLinux(action) : null
|
|
214
|
+
if (!result) {
|
|
215
|
+
return denied(`action ${action} is not supported on ${process.platform}`)
|
|
216
|
+
}
|
|
217
|
+
if (!result.ok) {
|
|
218
|
+
return invalidArg(`action ${action} failed: ${result.error || result.stderr || 'unknown error'}`)
|
|
219
|
+
}
|
|
220
|
+
return ok({
|
|
221
|
+
action,
|
|
222
|
+
host: hostname(),
|
|
223
|
+
platform: process.platform,
|
|
224
|
+
exitCode: result.code,
|
|
225
|
+
detail: result.detail || result.stdout || '',
|
|
226
|
+
stderr: result.stderr || '',
|
|
227
|
+
ms: Date.now() - started
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { hostname, platform, arch, release, uptime, type } from 'node:os'
|
|
2
|
+
import { basename } from 'node:path'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { MESH_VERSION, configPath } from '../core/config.js'
|
|
5
|
+
import { describeCert } from '../core/tls.js'
|
|
6
|
+
import { auditPath, auditSummary } from '../core/audit.js'
|
|
7
|
+
import { ok } from '../core/result.js'
|
|
8
|
+
|
|
9
|
+
/** Self-description — the first call a peer AI should make. */
|
|
10
|
+
export function buildCore(ctx) {
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
name: 'mesh_self',
|
|
14
|
+
title: 'Describe this mesh node',
|
|
15
|
+
description:
|
|
16
|
+
'Return identity and capability information about the peer machine you are connected to: node name, mesh version, host/OS facts, every tool this node exposes with whether your token may call it, ACL summary, shell policy, job queue stats and configured peers. Call this first to learn what the remote side can do.',
|
|
17
|
+
inputSchema: z.object({}),
|
|
18
|
+
handler: async () => {
|
|
19
|
+
const config = ctx.getConfig()
|
|
20
|
+
const names = ctx.toolNames()
|
|
21
|
+
const acl = config.acl || {}
|
|
22
|
+
const shell = config.shell || {}
|
|
23
|
+
return ok({
|
|
24
|
+
protocol: 'mcp/streamable-http',
|
|
25
|
+
mesh: {
|
|
26
|
+
name: config.node?.name || 'local',
|
|
27
|
+
version: MESH_VERSION,
|
|
28
|
+
home: ctx.home,
|
|
29
|
+
repo: process.cwd(),
|
|
30
|
+
configPath: configPath(ctx.home),
|
|
31
|
+
auditPath: auditPath(ctx.home)
|
|
32
|
+
},
|
|
33
|
+
host: {
|
|
34
|
+
hostname: hostname(),
|
|
35
|
+
platform: platform(),
|
|
36
|
+
type: type(),
|
|
37
|
+
release: release(),
|
|
38
|
+
arch: arch(),
|
|
39
|
+
node: process.version,
|
|
40
|
+
pid: process.pid,
|
|
41
|
+
/**
|
|
42
|
+
* Parent pid. Whether this node hangs under a Task Scheduler
|
|
43
|
+
* (svchost -s Schedule, logon autostart) or under a DSH/shell
|
|
44
|
+
* process (will die with a tree-kill) cannot be told from pid alone.
|
|
45
|
+
*/
|
|
46
|
+
ppid: process.ppid,
|
|
47
|
+
/** Seconds since THIS node process started (process.uptime). */
|
|
48
|
+
processUptimeSec: Math.round(process.uptime()),
|
|
49
|
+
/**
|
|
50
|
+
* Seconds since the OS booted (node:os.uptime) — NOT process age.
|
|
51
|
+
* Kept for backward compatibility; use processUptimeSec for liveness.
|
|
52
|
+
*/
|
|
53
|
+
uptimeSec: Math.round(uptime())
|
|
54
|
+
},
|
|
55
|
+
tools: names.map((name) => ({ name, allowed: ctx.isAllowed(config, name) })),
|
|
56
|
+
acl: {
|
|
57
|
+
allow: acl.allow || [],
|
|
58
|
+
deny: acl.deny || [],
|
|
59
|
+
fsRoots: (acl.fsRoots || []).map((r) => ({ path: r, basename: basename(r) })),
|
|
60
|
+
defaultPolicy: 'deny',
|
|
61
|
+
/** The token you used: its id and any narrowing that applies to you only. */
|
|
62
|
+
you: { tokenId: ctx.peerId || null, narrowing: ctx.peerAcl || null }
|
|
63
|
+
},
|
|
64
|
+
shell: {
|
|
65
|
+
enabled: shell.enabled === true,
|
|
66
|
+
allowCommands: shell.allowCommands || [],
|
|
67
|
+
timeoutMs: shell.timeoutMs ?? 60000
|
|
68
|
+
},
|
|
69
|
+
limits: config.limits || {},
|
|
70
|
+
tls: describeCert(ctx.home),
|
|
71
|
+
jobs: ctx.jobs.stats(),
|
|
72
|
+
audit: auditSummary(ctx.home),
|
|
73
|
+
peers: (config.peers || []).map((p) => ({ id: p.id, url: p.url, direction: p.direction || 'outbound' })),
|
|
74
|
+
now: new Date().toISOString()
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|