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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/package.json +43 -4
  4. package/src/cli.js +1246 -0
  5. package/src/core/acl.js +81 -0
  6. package/src/core/audit.js +75 -0
  7. package/src/core/autostart.js +119 -0
  8. package/src/core/cluster.js +341 -0
  9. package/src/core/config.js +622 -0
  10. package/src/core/desktop-proxy.js +85 -0
  11. package/src/core/ds-web.js +394 -0
  12. package/src/core/host-agent.js +263 -0
  13. package/src/core/jobs.js +259 -0
  14. package/src/core/pairing.js +141 -0
  15. package/src/core/peer-client.js +273 -0
  16. package/src/core/peer-doctor.js +294 -0
  17. package/src/core/peer-http.js +61 -0
  18. package/src/core/repair.js +93 -0
  19. package/src/core/restart-policy.js +21 -0
  20. package/src/core/result.js +29 -0
  21. package/src/core/sakura-api.js +103 -0
  22. package/src/core/session-profile.js +146 -0
  23. package/src/core/tls.js +160 -0
  24. package/src/core/tunnels.js +212 -0
  25. package/src/core/util.js +68 -0
  26. package/src/e2e-autostart.js +58 -0
  27. package/src/e2e-channel.js +384 -0
  28. package/src/e2e-cluster.js +417 -0
  29. package/src/e2e-host.js +411 -0
  30. package/src/e2e-onboard.js +232 -0
  31. package/src/e2e-pair.js +317 -0
  32. package/src/e2e-plugin.js +395 -0
  33. package/src/e2e-repair.js +297 -0
  34. package/src/index.js +20 -0
  35. package/src/peer-bridge.js +124 -0
  36. package/src/selftest.js +872 -0
  37. package/src/server.js +317 -0
  38. package/src/tools/alerts.js +80 -0
  39. package/src/tools/channel.js +253 -0
  40. package/src/tools/cluster.js +235 -0
  41. package/src/tools/config-patch.js +182 -0
  42. package/src/tools/control-apps.js +469 -0
  43. package/src/tools/control-path.js +160 -0
  44. package/src/tools/control.js +232 -0
  45. package/src/tools/core.js +79 -0
  46. package/src/tools/delegate.js +653 -0
  47. package/src/tools/fs.js +436 -0
  48. package/src/tools/git.js +266 -0
  49. package/src/tools/host.js +305 -0
  50. package/src/tools/index.js +122 -0
  51. package/src/tools/repair-fs.js +130 -0
  52. package/src/tools/repair.js +317 -0
  53. package/src/tools/session-history.js +60 -0
  54. package/src/tools/shell.js +66 -0
  55. 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
+ }