dsh-mesh-node 0.0.0-stage → 0.0.1
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 +340 -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/session-profile.js +146 -0
- package/src/core/tls.js +160 -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 +313 -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,66 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { isInsideRoots } from '../core/config.js'
|
|
5
|
+
import { clip, run, short } from '../core/util.js'
|
|
6
|
+
import { denied, invalidArg, ok } from '../core/result.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Escape hatch: run a command on the peer machine.
|
|
10
|
+
* Three locks must all be open: acl.allow lists `shell_run`, `shell.enabled` is true,
|
|
11
|
+
* and cwd stays inside fsRoots ∪ shell.cwdRoots. No shell interpreter is involved.
|
|
12
|
+
*/
|
|
13
|
+
export function buildShell(ctx) {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
name: 'shell_run',
|
|
17
|
+
title: 'Run a command on the peer machine (opt-in)',
|
|
18
|
+
description:
|
|
19
|
+
'Execute a program on the peer machine without a shell. REFUSED unless the peer enabled it (config.shell.enabled + acl allow). Returns exit code, stdout, stderr and duration. Prefer the structured tools (fs_*, git_*) when they can do the job.',
|
|
20
|
+
inputSchema: z.object({
|
|
21
|
+
command: z.string().describe('Program to run, e.g. "node" or "git". Not a shell string.'),
|
|
22
|
+
args: z.array(z.string()).optional().describe('Arguments passed verbatim to the program.'),
|
|
23
|
+
cwd: z.string().optional().describe('Working directory (defaults to the first fsRoot).'),
|
|
24
|
+
timeoutMs: z.number().int().min(100).max(600000).optional()
|
|
25
|
+
}),
|
|
26
|
+
handler: async ({ command, args, cwd, timeoutMs }, { signal } = {}) => {
|
|
27
|
+
const config = ctx.getConfig()
|
|
28
|
+
const shell = config.shell || {}
|
|
29
|
+
if (shell.enabled !== true) {
|
|
30
|
+
return denied('shell_run is disabled on this node (config.shell.enabled=false)')
|
|
31
|
+
}
|
|
32
|
+
const allowCommands = Array.isArray(shell.allowCommands) ? shell.allowCommands : []
|
|
33
|
+
if (allowCommands.length > 0 && !allowCommands.includes(command)) {
|
|
34
|
+
return denied(`command not in shell.allowCommands: ${command}`, { allowCommands })
|
|
35
|
+
}
|
|
36
|
+
const roots = [...(config.acl?.fsRoots || []), ...(shell.cwdRoots || [])]
|
|
37
|
+
const workdir = resolve(cwd ?? config.acl?.fsRoots?.[0] ?? process.cwd())
|
|
38
|
+
if (!isInsideRoots(workdir, roots)) {
|
|
39
|
+
return denied(`cwd is outside the allowed roots: ${workdir}`)
|
|
40
|
+
}
|
|
41
|
+
if (!existsSync(workdir) || !statSync(workdir).isDirectory()) {
|
|
42
|
+
return invalidArg(`cwd is not a directory: ${workdir}`)
|
|
43
|
+
}
|
|
44
|
+
const limit = Math.min(timeoutMs ?? shell.timeoutMs ?? 60000, 600000)
|
|
45
|
+
const started = Date.now()
|
|
46
|
+
const out = await run(command, args ?? [], { cwd: workdir, timeout: limit, signal })
|
|
47
|
+
const stdout = clip(out.stdout, 262144)
|
|
48
|
+
const stderr = clip(out.stderr, 65536)
|
|
49
|
+
return ok({
|
|
50
|
+
command,
|
|
51
|
+
args: args ?? [],
|
|
52
|
+
cwd: workdir,
|
|
53
|
+
exitCode: out.code,
|
|
54
|
+
timedOut: out.timedOut,
|
|
55
|
+
aborted: Boolean(signal?.aborted),
|
|
56
|
+
durationMs: Date.now() - started,
|
|
57
|
+
stdout: stdout.text,
|
|
58
|
+
stdoutTruncated: stdout.truncated,
|
|
59
|
+
stderr: stderr.text,
|
|
60
|
+
stderrTruncated: stderr.truncated,
|
|
61
|
+
error: out.error ? short(out.error.message, 500) : null
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tailAudit } from '../core/audit.js'
|
|
3
|
+
import { TERMINAL_STATES } from '../core/jobs.js'
|
|
4
|
+
import { denied, invalidArg, notFound, ok } from '../core/result.js'
|
|
5
|
+
|
|
6
|
+
const BLOCKED_JOB_TARGETS = /^(task_|mesh_)/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Async task queue. A long tool call submitted here returns a job id immediately;
|
|
10
|
+
* the caller polls `task_status`. Jobs are persisted, so a dropped connection or a
|
|
11
|
+
* client timeout never loses the work — and `idem` makes retries safe.
|
|
12
|
+
*/
|
|
13
|
+
export function buildTasks(ctx) {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
name: 'task_submit',
|
|
17
|
+
title: 'Run a long tool call asynchronously on the peer machine',
|
|
18
|
+
description:
|
|
19
|
+
'Submit any ACL-allowed tool (except task_* / mesh_*) as a background job and return a jobId immediately. Use this for slow operations (repo_scan, fs_grep on a big tree, shell_run) so the MCP call does not time out. Pass the same `idem` key when retrying to avoid duplicate work.',
|
|
20
|
+
inputSchema: z.object({
|
|
21
|
+
tool: z.string().describe('Name of the tool to run, e.g. "repo_scan".'),
|
|
22
|
+
arguments: z.record(z.string(), z.unknown()).optional().describe('Arguments for that tool.'),
|
|
23
|
+
idem: z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1)
|
|
26
|
+
.max(200)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Idempotency key: submitting twice with the same key returns the original job.'),
|
|
29
|
+
timeoutMs: z.number().int().min(1000).max(3600000).optional().describe('Abort the job after this long.')
|
|
30
|
+
}),
|
|
31
|
+
handler: async ({ tool, arguments: args, idem, timeoutMs }, meta = {}) => {
|
|
32
|
+
if (!tool || typeof tool !== 'string') return invalidArg('tool is required')
|
|
33
|
+
if (BLOCKED_JOB_TARGETS.test(tool)) {
|
|
34
|
+
return invalidArg(`refusing to queue a job that targets "${tool}" (job/self tools are not queueable)`)
|
|
35
|
+
}
|
|
36
|
+
const config = ctx.getConfig()
|
|
37
|
+
const known = ctx.toolNames().includes(tool)
|
|
38
|
+
if (!known) return notFound(`unknown tool: ${tool}`)
|
|
39
|
+
const allowed = ctx.isAllowed(config, tool)
|
|
40
|
+
if (!allowed) return denied(`${tool} is not allowed by local ACL`)
|
|
41
|
+
const limit = Math.min(timeoutMs ?? config.limits?.jobTimeoutMs ?? 300000, 3600000)
|
|
42
|
+
const { job, idempotent } = ctx.jobs.submit({
|
|
43
|
+
tool,
|
|
44
|
+
arguments: args ?? {},
|
|
45
|
+
idem,
|
|
46
|
+
peer: meta?.peerId,
|
|
47
|
+
timeoutMs: limit
|
|
48
|
+
})
|
|
49
|
+
return ok({
|
|
50
|
+
jobId: job.id,
|
|
51
|
+
state: job.state,
|
|
52
|
+
idempotent,
|
|
53
|
+
poll: 'task_status',
|
|
54
|
+
tool,
|
|
55
|
+
timeoutMs: limit
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'task_status',
|
|
61
|
+
title: 'Poll a background job',
|
|
62
|
+
description:
|
|
63
|
+
'Return one job record: state (queued|running|succeeded|failed|cancelled), timings, error and the tool result once finished. Poll this instead of re-running the tool.',
|
|
64
|
+
inputSchema: z.object({ jobId: z.string().describe('Job id returned by task_submit.') }),
|
|
65
|
+
handler: async ({ jobId }) => {
|
|
66
|
+
const job = ctx.jobs.get(jobId)
|
|
67
|
+
if (!job) return notFound(`no such job: ${jobId}`)
|
|
68
|
+
const done = TERMINAL_STATES.includes(job.state)
|
|
69
|
+
return ok({ ...job, done })
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'task_list',
|
|
74
|
+
title: 'List background jobs',
|
|
75
|
+
description: 'List recent background jobs on the peer machine, newest first, optionally filtered by state.',
|
|
76
|
+
inputSchema: z.object({
|
|
77
|
+
state: z.enum(['queued', 'running', 'succeeded', 'failed', 'cancelled']).optional(),
|
|
78
|
+
limit: z.number().int().min(1).max(100).optional()
|
|
79
|
+
}),
|
|
80
|
+
handler: async ({ state, limit }) => {
|
|
81
|
+
const jobs = ctx.jobs.list({ state, limit: limit ?? 20 })
|
|
82
|
+
return ok({ count: jobs.length, stats: ctx.jobs.stats(), jobs })
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'task_cancel',
|
|
87
|
+
title: 'Cancel a background job',
|
|
88
|
+
description:
|
|
89
|
+
'Abort a queued or running job. Running jobs receive an abort signal (long subprocesses are killed); finished jobs cannot be cancelled.',
|
|
90
|
+
inputSchema: z.object({ jobId: z.string().describe('Job id to cancel.') }),
|
|
91
|
+
handler: async ({ jobId }) => {
|
|
92
|
+
const result = ctx.jobs.cancel(jobId)
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
return result.code === 'E_NOT_FOUND'
|
|
95
|
+
? notFound(result.message)
|
|
96
|
+
: invalidArg(result.message)
|
|
97
|
+
}
|
|
98
|
+
return ok({ cancelled: true, job: result.job })
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'mesh_audit_tail',
|
|
103
|
+
title: 'Read this node audit log',
|
|
104
|
+
description:
|
|
105
|
+
'Return the most recent audit entries of this node (who called which tool, whether it succeeded, how long it took). Argument values are never logged, only their key names.',
|
|
106
|
+
inputSchema: z.object({
|
|
107
|
+
limit: z.number().int().min(1).max(400).optional(),
|
|
108
|
+
tool: z.string().optional(),
|
|
109
|
+
ok: z.boolean().optional()
|
|
110
|
+
}),
|
|
111
|
+
handler: async ({ limit, tool, ok: okFilter }) => {
|
|
112
|
+
const tail = tailAudit(ctx.home, { limit: limit ?? 50, tool, ok: okFilter })
|
|
113
|
+
return ok({ ...tail, jobs: ctx.jobs.stats() })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|