omniwire 2.3.0 → 2.4.0
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/README.md +426 -485
- package/dist/mcp/server.js +533 -39
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -36,6 +36,14 @@ function fmtExecOutput(result, timeoutSec) {
|
|
|
36
36
|
return `TIMEOUT ${timeoutSec}s\n${result.stdout || '(empty)'}`;
|
|
37
37
|
return `exit ${result.code}\n${result.stderr}`;
|
|
38
38
|
}
|
|
39
|
+
function fmtJson(node, result, label) {
|
|
40
|
+
return okBrief(JSON.stringify({
|
|
41
|
+
node, ok: result.code === 0, code: result.code, ms: result.durationMs,
|
|
42
|
+
...(label ? { label } : {}),
|
|
43
|
+
stdout: result.stdout.slice(0, MAX_OUTPUT),
|
|
44
|
+
...(result.stderr ? { stderr: result.stderr.slice(0, 1000) } : {}),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
39
47
|
function multiResult(results) {
|
|
40
48
|
const parts = results.map((r) => {
|
|
41
49
|
const mark = r.code === 0 ? 'ok' : `exit ${r.code}`;
|
|
@@ -46,30 +54,48 @@ function multiResult(results) {
|
|
|
46
54
|
});
|
|
47
55
|
return okBrief(trim(parts.join('\n\n')));
|
|
48
56
|
}
|
|
57
|
+
function multiResultJson(results) {
|
|
58
|
+
return okBrief(JSON.stringify(results.map((r) => ({
|
|
59
|
+
node: r.nodeId, ok: r.code === 0, code: r.code, ms: r.durationMs,
|
|
60
|
+
stdout: r.stdout.slice(0, MAX_OUTPUT),
|
|
61
|
+
...(r.stderr ? { stderr: r.stderr.slice(0, 500) } : {}),
|
|
62
|
+
}))));
|
|
63
|
+
}
|
|
64
|
+
// -- Agentic state -- shared across tool calls in the same MCP session --------
|
|
65
|
+
const resultStore = new Map(); // key -> value store for chaining
|
|
49
66
|
// -----------------------------------------------------------------------------
|
|
50
67
|
export function createOmniWireServer(manager, transfer) {
|
|
51
68
|
const server = new McpServer({
|
|
52
69
|
name: 'omniwire',
|
|
53
|
-
version: '2.
|
|
70
|
+
version: '2.4.0',
|
|
54
71
|
});
|
|
55
72
|
const shells = new ShellManager(manager);
|
|
56
73
|
const realtime = new RealtimeChannel(manager);
|
|
57
74
|
const tunnels = new TunnelManager(manager);
|
|
58
75
|
// --- Tool 1: omniwire_exec ---
|
|
59
|
-
server.tool('omniwire_exec', 'Execute a command on a
|
|
76
|
+
server.tool('omniwire_exec', 'Execute a command on a mesh node. Supports retry, assertions, JSON output, and result storage for agentic chaining.', {
|
|
60
77
|
node: z.string().optional().describe('Target node id (windows, contabo, hostinger, thinkpad). Auto-selects if omitted.'),
|
|
61
|
-
command: z.string().optional().describe('Shell command to run
|
|
78
|
+
command: z.string().optional().describe('Shell command to run. Use {{key}} to interpolate stored results from previous calls.'),
|
|
62
79
|
timeout: z.number().optional().describe('Timeout in seconds (default 30)'),
|
|
63
|
-
script: z.string().optional().describe('Multi-line script content. Sent as temp file via SFTP then executed.
|
|
64
|
-
label: z.string().optional().describe('Short label for the operation
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
script: z.string().optional().describe('Multi-line script content. Sent as temp file via SFTP then executed.'),
|
|
81
|
+
label: z.string().optional().describe('Short label for the operation. Max 60 chars.'),
|
|
82
|
+
format: z.enum(['text', 'json']).optional().describe('Output format. "json" returns structured {node, ok, code, ms, stdout, stderr} for programmatic parsing.'),
|
|
83
|
+
retry: z.number().optional().describe('Retry N times on failure (with 1s delay between). Default 0.'),
|
|
84
|
+
assert: z.string().optional().describe('Grep pattern to assert in stdout. If not found, returns error. Use for validation in agentic chains.'),
|
|
85
|
+
store_as: z.string().optional().describe('Store trimmed stdout under this key. Retrieve in subsequent calls via {{key}} in command.'),
|
|
86
|
+
}, async ({ node, command, timeout, script, label, format, retry, assert: assertPattern, store_as }) => {
|
|
68
87
|
if (!command && !script) {
|
|
69
88
|
return fail('either command or script is required');
|
|
70
89
|
}
|
|
71
90
|
const nodeId = node ?? 'contabo';
|
|
72
91
|
const timeoutSec = timeout ?? 30;
|
|
92
|
+
const maxRetries = retry ?? 0;
|
|
93
|
+
const useJson = format === 'json';
|
|
94
|
+
// Interpolate stored results: {{key}} -> value
|
|
95
|
+
let resolvedCmd = command;
|
|
96
|
+
if (resolvedCmd) {
|
|
97
|
+
resolvedCmd = resolvedCmd.replace(/\{\{(\w+)\}\}/g, (_, key) => resultStore.get(key) ?? `{{${key}}}`);
|
|
98
|
+
}
|
|
73
99
|
let effectiveCmd;
|
|
74
100
|
if (script) {
|
|
75
101
|
const tmpFile = `/tmp/.ow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -77,21 +103,43 @@ export function createOmniWireServer(manager, transfer) {
|
|
|
77
103
|
}
|
|
78
104
|
else {
|
|
79
105
|
effectiveCmd = timeoutSec < 300
|
|
80
|
-
? `timeout ${timeoutSec} bash -c '${
|
|
81
|
-
:
|
|
106
|
+
? `timeout ${timeoutSec} bash -c '${resolvedCmd.replace(/'/g, "'\\''")}'`
|
|
107
|
+
: resolvedCmd;
|
|
82
108
|
}
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
// Execute with retry
|
|
110
|
+
let result = await manager.exec(nodeId, effectiveCmd);
|
|
111
|
+
for (let attempt = 0; attempt < maxRetries && result.code !== 0; attempt++) {
|
|
112
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
113
|
+
result = await manager.exec(nodeId, effectiveCmd);
|
|
114
|
+
}
|
|
115
|
+
// Store result if requested
|
|
116
|
+
if (store_as && result.code === 0) {
|
|
117
|
+
resultStore.set(store_as, result.stdout.trim());
|
|
118
|
+
}
|
|
119
|
+
// Assert pattern in output
|
|
120
|
+
if (assertPattern && result.code === 0) {
|
|
121
|
+
const regex = new RegExp(assertPattern);
|
|
122
|
+
if (!regex.test(result.stdout)) {
|
|
123
|
+
return useJson
|
|
124
|
+
? okBrief(JSON.stringify({ node: nodeId, ok: false, code: -2, ms: result.durationMs, error: `assert failed: /${assertPattern}/ not found in stdout`, stdout: result.stdout.slice(0, 500) }))
|
|
125
|
+
: fail(`${nodeId} assert failed: /${assertPattern}/ not found`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return useJson
|
|
129
|
+
? fmtJson(nodeId, result, label ?? undefined)
|
|
130
|
+
: ok(nodeId, result.durationMs, fmtExecOutput(result, timeoutSec), label ?? undefined);
|
|
85
131
|
});
|
|
86
132
|
// --- Tool 2: omniwire_broadcast ---
|
|
87
133
|
server.tool('omniwire_broadcast', 'Execute a command on all online mesh nodes simultaneously.', {
|
|
88
|
-
command: z.string().describe('Shell command to run on all nodes'),
|
|
134
|
+
command: z.string().describe('Shell command to run on all nodes. Supports {{key}} interpolation.'),
|
|
89
135
|
nodes: z.array(z.string()).optional().describe('Subset of nodes to target. All online nodes if omitted.'),
|
|
90
|
-
|
|
136
|
+
format: z.enum(['text', 'json']).optional().describe('Output format.'),
|
|
137
|
+
}, async ({ command, nodes: targetNodes, format }) => {
|
|
138
|
+
const resolved = command.replace(/\{\{(\w+)\}\}/g, (_, key) => resultStore.get(key) ?? `{{${key}}}`);
|
|
91
139
|
const results = targetNodes
|
|
92
|
-
? await manager.execOn(targetNodes,
|
|
93
|
-
: await manager.execAll(
|
|
94
|
-
return multiResult(results);
|
|
140
|
+
? await manager.execOn(targetNodes, resolved)
|
|
141
|
+
: await manager.execAll(resolved);
|
|
142
|
+
return format === 'json' ? multiResultJson(results) : multiResult(results);
|
|
95
143
|
});
|
|
96
144
|
// --- Tool 3: omniwire_mesh_status ---
|
|
97
145
|
server.tool('omniwire_mesh_status', 'Get health and resource usage for all mesh nodes.', {}, async () => {
|
|
@@ -469,33 +517,66 @@ tags: ${meshNode.tags.join(', ')}`;
|
|
|
469
517
|
const result = await manager.exec(nodeId, wrappedCmd);
|
|
470
518
|
return ok(nodeId, result.durationMs, fmtExecOutput(result, timeoutSec), label ?? `${interp} script`);
|
|
471
519
|
});
|
|
472
|
-
// --- Tool 25: omniwire_batch (
|
|
473
|
-
server.tool('omniwire_batch', 'Run multiple commands
|
|
520
|
+
// --- Tool 25: omniwire_batch (chaining-aware multi-command) ---
|
|
521
|
+
server.tool('omniwire_batch', 'Run multiple commands in a single tool call. Supports chaining (sequential with {{prev}} interpolation), abort-on-fail, store_as, and JSON output. Use this to reduce agentic round-trips.', {
|
|
474
522
|
commands: z.array(z.object({
|
|
475
523
|
node: z.string().optional().describe('Node id (default: contabo)'),
|
|
476
|
-
command: z.string().describe('Command to
|
|
477
|
-
label: z.string().optional().describe('Short label
|
|
524
|
+
command: z.string().describe('Command. Use {{key}} to interpolate stored results, {{prev}} for previous command stdout.'),
|
|
525
|
+
label: z.string().optional().describe('Short label'),
|
|
526
|
+
store_as: z.string().optional().describe('Store stdout under this key for later use'),
|
|
478
527
|
})).describe('Array of commands to execute'),
|
|
479
|
-
parallel: z.boolean().optional().describe('Run
|
|
480
|
-
|
|
528
|
+
parallel: z.boolean().optional().describe('Run in parallel (default: true). Set false for sequential chaining with {{prev}}.'),
|
|
529
|
+
abort_on_fail: z.boolean().optional().describe('Stop executing remaining commands if one fails (sequential mode only). Default: false.'),
|
|
530
|
+
format: z.enum(['text', 'json']).optional().describe('Output format.'),
|
|
531
|
+
}, async ({ commands, parallel, abort_on_fail, format }) => {
|
|
481
532
|
const runParallel = parallel !== false;
|
|
482
|
-
const
|
|
533
|
+
const useJson = format === 'json';
|
|
534
|
+
if (runParallel) {
|
|
535
|
+
const execute = async (item) => {
|
|
536
|
+
const nodeId = item.node ?? 'contabo';
|
|
537
|
+
const resolved = item.command.replace(/\{\{(\w+)\}\}/g, (_, key) => resultStore.get(key) ?? `{{${key}}}`);
|
|
538
|
+
const result = await manager.exec(nodeId, resolved);
|
|
539
|
+
if (item.store_as && result.code === 0)
|
|
540
|
+
resultStore.set(item.store_as, result.stdout.trim());
|
|
541
|
+
if (useJson) {
|
|
542
|
+
return JSON.stringify({ node: nodeId, ok: result.code === 0, code: result.code, ms: result.durationMs, label: item.label, stdout: result.stdout.slice(0, 2000), ...(result.stderr ? { stderr: result.stderr.slice(0, 500) } : {}) });
|
|
543
|
+
}
|
|
544
|
+
const lbl = item.label ?? item.command.slice(0, 40);
|
|
545
|
+
const body = result.code === 0
|
|
546
|
+
? (result.stdout || '(empty)').split('\n').slice(0, 20).join('\n')
|
|
547
|
+
: `exit ${result.code}: ${result.stderr.split('\n').slice(0, 5).join('\n')}`;
|
|
548
|
+
return `-- ${nodeId} > ${lbl} ${t(result.durationMs)}\n${body}`;
|
|
549
|
+
};
|
|
550
|
+
const results = await Promise.all(commands.map(execute));
|
|
551
|
+
return useJson ? okBrief(`[${results.join(',')}]`) : okBrief(trim(results.join('\n\n')));
|
|
552
|
+
}
|
|
553
|
+
// Sequential with chaining
|
|
554
|
+
const outputs = [];
|
|
555
|
+
let prevStdout = '';
|
|
556
|
+
for (const item of commands) {
|
|
483
557
|
const nodeId = item.node ?? 'contabo';
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
558
|
+
let resolved = item.command.replace(/\{\{prev\}\}/g, prevStdout.trim());
|
|
559
|
+
resolved = resolved.replace(/\{\{(\w+)\}\}/g, (_, key) => resultStore.get(key) ?? `{{${key}}}`);
|
|
560
|
+
const result = await manager.exec(nodeId, resolved);
|
|
561
|
+
prevStdout = result.stdout;
|
|
562
|
+
if (item.store_as && result.code === 0)
|
|
563
|
+
resultStore.set(item.store_as, result.stdout.trim());
|
|
564
|
+
if (useJson) {
|
|
565
|
+
outputs.push(JSON.stringify({ node: nodeId, ok: result.code === 0, code: result.code, ms: result.durationMs, label: item.label, stdout: result.stdout.slice(0, 2000), ...(result.stderr ? { stderr: result.stderr.slice(0, 500) } : {}) }));
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const lbl = item.label ?? item.command.slice(0, 40);
|
|
569
|
+
const body = result.code === 0
|
|
570
|
+
? (result.stdout || '(empty)').split('\n').slice(0, 20).join('\n')
|
|
571
|
+
: `exit ${result.code}: ${result.stderr.split('\n').slice(0, 5).join('\n')}`;
|
|
572
|
+
outputs.push(`-- ${nodeId} > ${lbl} ${t(result.durationMs)}\n${body}`);
|
|
573
|
+
}
|
|
574
|
+
if (abort_on_fail && result.code !== 0) {
|
|
575
|
+
const msg = useJson ? `[${outputs.join(',')}]` : outputs.join('\n\n') + '\n\n-- ABORTED (command failed)';
|
|
576
|
+
return okBrief(trim(msg));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return useJson ? okBrief(`[${outputs.join(',')}]`) : okBrief(trim(outputs.join('\n\n')));
|
|
499
580
|
});
|
|
500
581
|
// --- Tool 26: omniwire_update ---
|
|
501
582
|
server.tool('omniwire_update', 'Check for updates and self-update OmniWire to the latest version.', {
|
|
@@ -662,6 +743,419 @@ tags: ${meshNode.tags.join(', ')}`;
|
|
|
662
743
|
const label = unit ? `syslog ${unit}` : 'syslog';
|
|
663
744
|
return ok(node, result.durationMs, result.code === 0 ? result.stdout : result.stderr, label);
|
|
664
745
|
});
|
|
746
|
+
// =========================================================================
|
|
747
|
+
// AGENTIC / A2A / MULTI-AGENT TOOLS
|
|
748
|
+
// =========================================================================
|
|
749
|
+
// --- Tool 33: omniwire_store ---
|
|
750
|
+
server.tool('omniwire_store', 'Key-value store for chaining results across tool calls in the same session. Agents can store intermediate results and retrieve them later. Keys persist until session ends.', {
|
|
751
|
+
action: z.enum(['get', 'set', 'delete', 'list', 'clear']).describe('Action'),
|
|
752
|
+
key: z.string().optional().describe('Key name (required for get/set/delete)'),
|
|
753
|
+
value: z.string().optional().describe('Value to store (for set)'),
|
|
754
|
+
}, async ({ action, key, value }) => {
|
|
755
|
+
switch (action) {
|
|
756
|
+
case 'get':
|
|
757
|
+
if (!key)
|
|
758
|
+
return fail('key required');
|
|
759
|
+
return okBrief(resultStore.get(key) ?? '(not found)');
|
|
760
|
+
case 'set':
|
|
761
|
+
if (!key || value === undefined)
|
|
762
|
+
return fail('key and value required');
|
|
763
|
+
resultStore.set(key, value);
|
|
764
|
+
return okBrief(`stored ${key} (${value.length} chars)`);
|
|
765
|
+
case 'delete':
|
|
766
|
+
if (!key)
|
|
767
|
+
return fail('key required');
|
|
768
|
+
resultStore.delete(key);
|
|
769
|
+
return okBrief(`deleted ${key}`);
|
|
770
|
+
case 'list':
|
|
771
|
+
if (resultStore.size === 0)
|
|
772
|
+
return okBrief('(empty store)');
|
|
773
|
+
return okBrief([...resultStore.entries()].map(([k, v]) => `${k} = ${v.slice(0, 80)}${v.length > 80 ? '...' : ''}`).join('\n'));
|
|
774
|
+
case 'clear':
|
|
775
|
+
resultStore.clear();
|
|
776
|
+
return okBrief('store cleared');
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
// --- Tool 34: omniwire_pipeline ---
|
|
780
|
+
server.tool('omniwire_pipeline', 'Execute a multi-step pipeline across nodes. Each step can depend on previous step output. Steps run sequentially on potentially different nodes. Pipeline aborts on first failure unless ignore_errors is set. Designed for multi-agent orchestration.', {
|
|
781
|
+
steps: z.array(z.object({
|
|
782
|
+
node: z.string().optional().describe('Node (default: contabo)'),
|
|
783
|
+
command: z.string().describe('Command. Use {{prev}} for previous stdout, {{stepN}} for step N output, {{key}} for store.'),
|
|
784
|
+
label: z.string().optional().describe('Step label'),
|
|
785
|
+
store_as: z.string().optional().describe('Store stdout under this key'),
|
|
786
|
+
on_fail: z.enum(['abort', 'skip', 'continue']).optional().describe('Behavior on failure (default: abort)'),
|
|
787
|
+
})).describe('Pipeline steps'),
|
|
788
|
+
format: z.enum(['text', 'json']).optional(),
|
|
789
|
+
}, async ({ steps, format }) => {
|
|
790
|
+
const useJson = format === 'json';
|
|
791
|
+
const stepOutputs = [];
|
|
792
|
+
const results = [];
|
|
793
|
+
let prevStdout = '';
|
|
794
|
+
for (let i = 0; i < steps.length; i++) {
|
|
795
|
+
const step = steps[i];
|
|
796
|
+
const nodeId = step.node ?? 'contabo';
|
|
797
|
+
let cmd = step.command
|
|
798
|
+
.replace(/\{\{prev\}\}/g, prevStdout.trim())
|
|
799
|
+
.replace(/\{\{step(\d+)\}\}/g, (_, n) => stepOutputs[parseInt(n)] ?? '')
|
|
800
|
+
.replace(/\{\{(\w+)\}\}/g, (_, key) => resultStore.get(key) ?? `{{${key}}}`);
|
|
801
|
+
const result = await manager.exec(nodeId, cmd);
|
|
802
|
+
prevStdout = result.stdout;
|
|
803
|
+
stepOutputs[i] = result.stdout.trim();
|
|
804
|
+
if (step.store_as && result.code === 0)
|
|
805
|
+
resultStore.set(step.store_as, result.stdout.trim());
|
|
806
|
+
results.push({
|
|
807
|
+
step: i, node: nodeId, label: step.label, ok: result.code === 0,
|
|
808
|
+
code: result.code, ms: result.durationMs,
|
|
809
|
+
stdout: result.stdout.slice(0, 2000),
|
|
810
|
+
...(result.stderr ? { stderr: result.stderr.slice(0, 500) } : {}),
|
|
811
|
+
});
|
|
812
|
+
if (result.code !== 0) {
|
|
813
|
+
const onFail = step.on_fail ?? 'abort';
|
|
814
|
+
if (onFail === 'abort')
|
|
815
|
+
break;
|
|
816
|
+
if (onFail === 'skip') {
|
|
817
|
+
prevStdout = '';
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (useJson)
|
|
823
|
+
return okBrief(JSON.stringify(results));
|
|
824
|
+
const lines = results.map((r) => {
|
|
825
|
+
const status = r.ok ? 'ok' : `exit ${r.code}`;
|
|
826
|
+
const lbl = r.label ?? `step ${r.step}`;
|
|
827
|
+
const body = r.ok ? r.stdout.split('\n').slice(0, 10).join('\n') : (r.stderr ?? '').split('\n').slice(0, 5).join('\n');
|
|
828
|
+
return `[${r.step}] ${r.node} > ${lbl} ${t(r.ms)} ${status}\n${body}`;
|
|
829
|
+
});
|
|
830
|
+
return okBrief(trim(lines.join('\n\n')));
|
|
831
|
+
});
|
|
832
|
+
// --- Tool 35: omniwire_watch ---
|
|
833
|
+
server.tool('omniwire_watch', 'Poll a command until a condition is met or timeout. Useful for waiting on deployments, services starting, builds completing. Returns when the assert pattern matches stdout.', {
|
|
834
|
+
node: z.string().optional().describe('Node (default: contabo)'),
|
|
835
|
+
command: z.string().describe('Command to poll'),
|
|
836
|
+
assert: z.string().describe('Regex pattern to match in stdout. Returns success when found.'),
|
|
837
|
+
interval: z.number().optional().describe('Poll interval in seconds (default: 3)'),
|
|
838
|
+
timeout: z.number().optional().describe('Max wait in seconds (default: 60)'),
|
|
839
|
+
label: z.string().optional(),
|
|
840
|
+
store_as: z.string().optional().describe('Store matching stdout on success'),
|
|
841
|
+
}, async ({ node, command, assert: pattern, interval, timeout, label, store_as }) => {
|
|
842
|
+
const nodeId = node ?? 'contabo';
|
|
843
|
+
const intervalMs = (interval ?? 3) * 1000;
|
|
844
|
+
const timeoutMs = (timeout ?? 60) * 1000;
|
|
845
|
+
const regex = new RegExp(pattern);
|
|
846
|
+
const start = Date.now();
|
|
847
|
+
while (Date.now() - start < timeoutMs) {
|
|
848
|
+
const result = await manager.exec(nodeId, command);
|
|
849
|
+
if (result.code === 0 && regex.test(result.stdout)) {
|
|
850
|
+
if (store_as)
|
|
851
|
+
resultStore.set(store_as, result.stdout.trim());
|
|
852
|
+
return ok(nodeId, Date.now() - start, result.stdout, label ?? `watch (matched after ${t(Date.now() - start)})`);
|
|
853
|
+
}
|
|
854
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
855
|
+
}
|
|
856
|
+
return fail(`${nodeId} watch timeout after ${t(timeoutMs)}: /${pattern}/ never matched`);
|
|
857
|
+
});
|
|
858
|
+
// --- Tool 36: omniwire_healthcheck ---
|
|
859
|
+
server.tool('omniwire_healthcheck', 'Run a comprehensive health check across all nodes. Returns structured per-node status with connectivity, disk, memory, load, and service checks. Single tool call replaces 4+ individual calls.', {
|
|
860
|
+
checks: z.array(z.enum(['connectivity', 'disk', 'memory', 'load', 'docker', 'services'])).optional().describe('Which checks to run (default: all)'),
|
|
861
|
+
nodes: z.array(z.string()).optional().describe('Nodes to check (default: all online)'),
|
|
862
|
+
format: z.enum(['text', 'json']).optional(),
|
|
863
|
+
}, async ({ checks, nodes: targetNodes, format }) => {
|
|
864
|
+
const checkList = checks ?? ['connectivity', 'disk', 'memory', 'load'];
|
|
865
|
+
const useJson = format === 'json';
|
|
866
|
+
const parts = [];
|
|
867
|
+
if (checkList.includes('connectivity'))
|
|
868
|
+
parts.push("echo 'CONN:ok'");
|
|
869
|
+
if (checkList.includes('disk'))
|
|
870
|
+
parts.push("echo -n 'DISK:'; df / --output=pcent | tail -1 | tr -d ' %'");
|
|
871
|
+
if (checkList.includes('memory'))
|
|
872
|
+
parts.push("echo -n 'MEM:'; free | awk '/Mem:/{printf \"%.0f\", $3/$2*100}'");
|
|
873
|
+
if (checkList.includes('load'))
|
|
874
|
+
parts.push("echo -n 'LOAD:'; cat /proc/loadavg | awk '{print $1}'");
|
|
875
|
+
if (checkList.includes('docker'))
|
|
876
|
+
parts.push("echo -n 'DOCKER:'; docker ps -q 2>/dev/null | wc -l | tr -d ' '");
|
|
877
|
+
if (checkList.includes('services'))
|
|
878
|
+
parts.push("echo -n 'SVCFAIL:'; systemctl --failed --no-legend 2>/dev/null | wc -l | tr -d ' '");
|
|
879
|
+
const cmd = parts.join('; echo; ');
|
|
880
|
+
const nodeIds = targetNodes ?? manager.getOnlineNodes();
|
|
881
|
+
const results = await Promise.all(nodeIds.map((id) => manager.exec(id, cmd)));
|
|
882
|
+
if (useJson) {
|
|
883
|
+
const parsed = results.map((r) => {
|
|
884
|
+
const data = { node: r.nodeId, online: r.code !== -1 };
|
|
885
|
+
for (const line of r.stdout.split('\n')) {
|
|
886
|
+
const [k, v] = line.split(':');
|
|
887
|
+
if (k && v)
|
|
888
|
+
data[k.toLowerCase()] = isNaN(Number(v)) ? v : Number(v);
|
|
889
|
+
}
|
|
890
|
+
data.ms = r.durationMs;
|
|
891
|
+
return data;
|
|
892
|
+
});
|
|
893
|
+
return okBrief(JSON.stringify(parsed));
|
|
894
|
+
}
|
|
895
|
+
const lines = results.map((r) => {
|
|
896
|
+
if (r.code === -1)
|
|
897
|
+
return `- ${r.nodeId.padEnd(10)} OFFLINE`;
|
|
898
|
+
const metrics = r.stdout.split('\n').filter(Boolean).join(' ');
|
|
899
|
+
return `+ ${r.nodeId.padEnd(10)} ${t(r.durationMs).padStart(6)} ${metrics}`;
|
|
900
|
+
});
|
|
901
|
+
return okBrief(lines.join('\n'));
|
|
902
|
+
});
|
|
903
|
+
// --- Tool 37: omniwire_agent_task ---
|
|
904
|
+
server.tool('omniwire_agent_task', 'Dispatch a task to a specific node for background execution and retrieve results later. Creates a task file on the node, runs it in background, and provides a task ID for polling. Designed for A2A (agent-to-agent) workflows where one agent dispatches work and another retrieves results.', {
|
|
905
|
+
action: z.enum(['dispatch', 'status', 'result', 'list', 'cancel']).describe('Action'),
|
|
906
|
+
node: z.string().optional().describe('Node (default: contabo)'),
|
|
907
|
+
command: z.string().optional().describe('Command to dispatch (for dispatch action)'),
|
|
908
|
+
task_id: z.string().optional().describe('Task ID (for status/result/cancel)'),
|
|
909
|
+
label: z.string().optional(),
|
|
910
|
+
}, async ({ action, node, command, task_id, label }) => {
|
|
911
|
+
const nodeId = node ?? 'contabo';
|
|
912
|
+
const taskDir = '/tmp/.omniwire-tasks';
|
|
913
|
+
if (action === 'dispatch') {
|
|
914
|
+
if (!command)
|
|
915
|
+
return fail('command required');
|
|
916
|
+
const id = `ow-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
917
|
+
const escaped = command.replace(/'/g, "'\\''");
|
|
918
|
+
const script = `mkdir -p ${taskDir} && echo 'running' > ${taskDir}/${id}.status && echo '${label ?? command.slice(0, 60)}' > ${taskDir}/${id}.label && (bash -c '${escaped}' > ${taskDir}/${id}.stdout 2> ${taskDir}/${id}.stderr; echo $? > ${taskDir}/${id}.exit; echo 'done' > ${taskDir}/${id}.status) &`;
|
|
919
|
+
const result = await manager.exec(nodeId, script);
|
|
920
|
+
return result.code === 0
|
|
921
|
+
? okBrief(`${nodeId} task dispatched: ${id}`)
|
|
922
|
+
: fail(`dispatch failed: ${result.stderr}`);
|
|
923
|
+
}
|
|
924
|
+
if (action === 'status' && task_id) {
|
|
925
|
+
const result = await manager.exec(nodeId, `cat ${taskDir}/${task_id}.status 2>/dev/null || echo 'not found'`);
|
|
926
|
+
return okBrief(`${nodeId} ${task_id}: ${result.stdout.trim()}`);
|
|
927
|
+
}
|
|
928
|
+
if (action === 'result' && task_id) {
|
|
929
|
+
const result = await manager.exec(nodeId, `echo "EXIT:$(cat ${taskDir}/${task_id}.exit 2>/dev/null)"; echo "---STDOUT---"; cat ${taskDir}/${task_id}.stdout 2>/dev/null; echo "---STDERR---"; cat ${taskDir}/${task_id}.stderr 2>/dev/null`);
|
|
930
|
+
return ok(nodeId, result.durationMs, result.stdout, `task ${task_id}`);
|
|
931
|
+
}
|
|
932
|
+
if (action === 'list') {
|
|
933
|
+
const result = await manager.exec(nodeId, `for f in ${taskDir}/*.status 2>/dev/null; do id=$(basename "$f" .status); echo "$id $(cat "$f") $(cat ${taskDir}/$id.label 2>/dev/null)"; done 2>/dev/null | tail -20`);
|
|
934
|
+
return ok(nodeId, result.durationMs, result.stdout || '(no tasks)', 'task list');
|
|
935
|
+
}
|
|
936
|
+
if (action === 'cancel' && task_id) {
|
|
937
|
+
await manager.exec(nodeId, `echo 'cancelled' > ${taskDir}/${task_id}.status`);
|
|
938
|
+
return okBrief(`${nodeId} ${task_id} cancelled`);
|
|
939
|
+
}
|
|
940
|
+
return fail('invalid action/params');
|
|
941
|
+
});
|
|
942
|
+
// --- Tool 38: omniwire_a2a_message ---
|
|
943
|
+
server.tool('omniwire_a2a_message', 'Agent-to-agent messaging via shared message queues on mesh nodes. Agents can send/receive typed messages, enabling multi-agent coordination without direct coupling. Messages are stored on disk and survive process restarts.', {
|
|
944
|
+
action: z.enum(['send', 'receive', 'peek', 'list_channels', 'clear']).describe('Action'),
|
|
945
|
+
channel: z.string().optional().describe('Message channel name (e.g., "recon-results", "scan-tasks")'),
|
|
946
|
+
node: z.string().optional().describe('Node hosting the queue (default: contabo)'),
|
|
947
|
+
message: z.string().optional().describe('Message content (for send). Can be JSON.'),
|
|
948
|
+
sender: z.string().optional().describe('Sender agent name (for send)'),
|
|
949
|
+
count: z.number().optional().describe('Number of messages to receive (default: 1). Messages are dequeued on receive.'),
|
|
950
|
+
}, async ({ action, channel, node, message, sender, count }) => {
|
|
951
|
+
const nodeId = node ?? 'contabo';
|
|
952
|
+
const queueDir = '/tmp/.omniwire-a2a';
|
|
953
|
+
if (action === 'send') {
|
|
954
|
+
if (!channel || !message)
|
|
955
|
+
return fail('channel and message required');
|
|
956
|
+
const ts = Date.now();
|
|
957
|
+
const id = `${ts}-${Math.random().toString(36).slice(2, 6)}`;
|
|
958
|
+
const payload = JSON.stringify({ id, ts, sender: sender ?? 'unknown', message });
|
|
959
|
+
const escaped = payload.replace(/'/g, "'\\''");
|
|
960
|
+
const result = await manager.exec(nodeId, `mkdir -p ${queueDir}/${channel} && echo '${escaped}' >> ${queueDir}/${channel}/queue`);
|
|
961
|
+
return result.code === 0
|
|
962
|
+
? okBrief(`${channel}: message sent (${message.length} chars)`)
|
|
963
|
+
: fail(result.stderr);
|
|
964
|
+
}
|
|
965
|
+
if (action === 'receive') {
|
|
966
|
+
if (!channel)
|
|
967
|
+
return fail('channel required');
|
|
968
|
+
const n = count ?? 1;
|
|
969
|
+
const result = await manager.exec(nodeId, `head -${n} ${queueDir}/${channel}/queue 2>/dev/null && sed -i '1,${n}d' ${queueDir}/${channel}/queue 2>/dev/null || echo '(empty queue)'`);
|
|
970
|
+
return ok(nodeId, result.durationMs, result.stdout, `a2a recv ${channel}`);
|
|
971
|
+
}
|
|
972
|
+
if (action === 'peek') {
|
|
973
|
+
if (!channel)
|
|
974
|
+
return fail('channel required');
|
|
975
|
+
const n = count ?? 5;
|
|
976
|
+
const result = await manager.exec(nodeId, `head -${n} ${queueDir}/${channel}/queue 2>/dev/null || echo '(empty queue)'`);
|
|
977
|
+
return ok(nodeId, result.durationMs, result.stdout, `a2a peek ${channel}`);
|
|
978
|
+
}
|
|
979
|
+
if (action === 'list_channels') {
|
|
980
|
+
const result = await manager.exec(nodeId, `ls -1 ${queueDir}/ 2>/dev/null || echo '(no channels)'`);
|
|
981
|
+
return ok(nodeId, result.durationMs, result.stdout, 'a2a channels');
|
|
982
|
+
}
|
|
983
|
+
if (action === 'clear') {
|
|
984
|
+
if (!channel)
|
|
985
|
+
return fail('channel required');
|
|
986
|
+
await manager.exec(nodeId, `rm -f ${queueDir}/${channel}/queue`);
|
|
987
|
+
return okBrief(`${channel}: cleared`);
|
|
988
|
+
}
|
|
989
|
+
return fail('invalid action');
|
|
990
|
+
});
|
|
991
|
+
// --- Tool 39: omniwire_semaphore ---
|
|
992
|
+
server.tool('omniwire_semaphore', 'Distributed locking / semaphore for multi-agent coordination. Prevents race conditions when multiple agents operate on the same resource. Uses atomic file-based locks on mesh nodes.', {
|
|
993
|
+
action: z.enum(['acquire', 'release', 'status', 'list']).describe('Action'),
|
|
994
|
+
lock_name: z.string().optional().describe('Lock name (e.g., "deploy-prod", "db-migration")'),
|
|
995
|
+
node: z.string().optional().describe('Node hosting the lock (default: contabo)'),
|
|
996
|
+
owner: z.string().optional().describe('Owner/agent name (for acquire)'),
|
|
997
|
+
ttl: z.number().optional().describe('Lock TTL in seconds (default: 300). Auto-releases after TTL.'),
|
|
998
|
+
}, async ({ action, lock_name, node, owner, ttl }) => {
|
|
999
|
+
const nodeId = node ?? 'contabo';
|
|
1000
|
+
const lockDir = '/tmp/.omniwire-locks';
|
|
1001
|
+
const ttlSec = ttl ?? 300;
|
|
1002
|
+
if (action === 'acquire') {
|
|
1003
|
+
if (!lock_name)
|
|
1004
|
+
return fail('lock_name required');
|
|
1005
|
+
const lockFile = `${lockDir}/${lock_name}.lock`;
|
|
1006
|
+
const ownerName = owner ?? 'agent';
|
|
1007
|
+
const now = Date.now();
|
|
1008
|
+
// Simple atomic lock: mkdir as atomic test-and-set, write owner info inside
|
|
1009
|
+
const acquireScript = [
|
|
1010
|
+
`mkdir -p ${lockDir}`,
|
|
1011
|
+
`if mkdir ${lockFile}.d 2>/dev/null; then`,
|
|
1012
|
+
` echo '${ownerName}:${now}:${ttlSec}' > ${lockFile}`,
|
|
1013
|
+
` echo 'acquired'`,
|
|
1014
|
+
`else`,
|
|
1015
|
+
` cat ${lockFile} 2>/dev/null || echo 'locked (unknown owner)'`,
|
|
1016
|
+
`fi`,
|
|
1017
|
+
].join('\n');
|
|
1018
|
+
const result = await manager.exec(nodeId, acquireScript);
|
|
1019
|
+
return okBrief(`${lock_name}: ${result.stdout.trim()}`);
|
|
1020
|
+
}
|
|
1021
|
+
if (action === 'release') {
|
|
1022
|
+
if (!lock_name)
|
|
1023
|
+
return fail('lock_name required');
|
|
1024
|
+
await manager.exec(nodeId, `rm -f ${lockDir}/${lock_name}.lock && rmdir ${lockDir}/${lock_name}.lock.d 2>/dev/null`);
|
|
1025
|
+
return okBrief(`${lock_name}: released`);
|
|
1026
|
+
}
|
|
1027
|
+
if (action === 'status') {
|
|
1028
|
+
if (!lock_name)
|
|
1029
|
+
return fail('lock_name required');
|
|
1030
|
+
const result = await manager.exec(nodeId, `cat ${lockDir}/${lock_name}.lock 2>/dev/null || echo '(unlocked)'`);
|
|
1031
|
+
return okBrief(`${lock_name}: ${result.stdout.trim()}`);
|
|
1032
|
+
}
|
|
1033
|
+
if (action === 'list') {
|
|
1034
|
+
const cmd = "for f in " + lockDir + "/*.lock 2>/dev/null; do [ -f \"$f\" ] && echo \"$(basename $f .lock): $(cat $f)\"; done 2>/dev/null || echo '(no locks)'";
|
|
1035
|
+
const result = await manager.exec(nodeId, cmd);
|
|
1036
|
+
return ok(nodeId, result.durationMs, result.stdout, 'locks');
|
|
1037
|
+
}
|
|
1038
|
+
return fail('invalid action');
|
|
1039
|
+
});
|
|
1040
|
+
// --- Tool 40: omniwire_event ---
|
|
1041
|
+
server.tool('omniwire_event', 'Publish/subscribe events for agent coordination. Agents can emit events and other agents can poll for them. Events are timestamped and stored in a log for audit. Supports the ACP/A2A event-driven pattern.', {
|
|
1042
|
+
action: z.enum(['emit', 'poll', 'history', 'clear']).describe('Action'),
|
|
1043
|
+
topic: z.string().optional().describe('Event topic (e.g., "deploy.complete", "scan.found-vuln")'),
|
|
1044
|
+
node: z.string().optional().describe('Node hosting events (default: contabo)'),
|
|
1045
|
+
data: z.string().optional().describe('Event data/payload (for emit). Can be JSON.'),
|
|
1046
|
+
source: z.string().optional().describe('Source agent name (for emit)'),
|
|
1047
|
+
since: z.string().optional().describe('Only return events after this timestamp (epoch ms) for poll'),
|
|
1048
|
+
limit: z.number().optional().describe('Max events to return (default: 10)'),
|
|
1049
|
+
}, async ({ action, topic, node, data, source, since, limit }) => {
|
|
1050
|
+
const nodeId = node ?? 'contabo';
|
|
1051
|
+
const eventDir = '/tmp/.omniwire-events';
|
|
1052
|
+
const n = limit ?? 10;
|
|
1053
|
+
if (action === 'emit') {
|
|
1054
|
+
if (!topic)
|
|
1055
|
+
return fail('topic required');
|
|
1056
|
+
const event = JSON.stringify({ ts: Date.now(), topic, source: source ?? 'agent', data: data ?? '' });
|
|
1057
|
+
const escaped = event.replace(/'/g, "'\\''");
|
|
1058
|
+
await manager.exec(nodeId, `mkdir -p ${eventDir} && echo '${escaped}' >> ${eventDir}/events.log`);
|
|
1059
|
+
return okBrief(`event emitted: ${topic}`);
|
|
1060
|
+
}
|
|
1061
|
+
if (action === 'poll') {
|
|
1062
|
+
let cmd;
|
|
1063
|
+
if (topic && since) {
|
|
1064
|
+
cmd = `grep '"topic":"${topic}"' ${eventDir}/events.log 2>/dev/null | awk -F'"ts":' '{split($2,a,","); if(a[1]>${since}) print}' | tail -${n}`;
|
|
1065
|
+
}
|
|
1066
|
+
else if (topic) {
|
|
1067
|
+
cmd = `grep '"topic":"${topic}"' ${eventDir}/events.log 2>/dev/null | tail -${n}`;
|
|
1068
|
+
}
|
|
1069
|
+
else if (since) {
|
|
1070
|
+
cmd = `awk -F'"ts":' '{split($2,a,","); if(a[1]>${since}) print}' ${eventDir}/events.log 2>/dev/null | tail -${n}`;
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
cmd = `tail -${n} ${eventDir}/events.log 2>/dev/null || echo '(no events)'`;
|
|
1074
|
+
}
|
|
1075
|
+
const result = await manager.exec(nodeId, cmd);
|
|
1076
|
+
return ok(nodeId, result.durationMs, result.stdout || '(no events)', `events ${topic ?? 'all'}`);
|
|
1077
|
+
}
|
|
1078
|
+
if (action === 'history') {
|
|
1079
|
+
const result = await manager.exec(nodeId, `wc -l ${eventDir}/events.log 2>/dev/null | awk '{print $1}'`);
|
|
1080
|
+
const count = result.stdout.trim() || '0';
|
|
1081
|
+
return okBrief(`${count} events total`);
|
|
1082
|
+
}
|
|
1083
|
+
if (action === 'clear') {
|
|
1084
|
+
await manager.exec(nodeId, `rm -f ${eventDir}/events.log`);
|
|
1085
|
+
return okBrief('events cleared');
|
|
1086
|
+
}
|
|
1087
|
+
return fail('invalid action');
|
|
1088
|
+
});
|
|
1089
|
+
// --- Tool 41: omniwire_workflow ---
|
|
1090
|
+
server.tool('omniwire_workflow', 'Define and execute a named workflow (DAG of steps) that can be reused. Workflows are stored on disk and can be triggered by any agent. Supports conditional steps, fan-out/fan-in, and cross-node orchestration.', {
|
|
1091
|
+
action: z.enum(['define', 'run', 'list', 'get', 'delete']).describe('Action'),
|
|
1092
|
+
name: z.string().optional().describe('Workflow name'),
|
|
1093
|
+
node: z.string().optional().describe('Node to store/run workflow (default: contabo)'),
|
|
1094
|
+
definition: z.string().optional().describe('JSON workflow definition for define action. Format: {steps: [{node, command, label, depends_on?, store_as?}]}'),
|
|
1095
|
+
format: z.enum(['text', 'json']).optional(),
|
|
1096
|
+
}, async ({ action, name, node, definition, format }) => {
|
|
1097
|
+
const nodeId = node ?? 'contabo';
|
|
1098
|
+
const wfDir = '/tmp/.omniwire-workflows';
|
|
1099
|
+
const useJson = format === 'json';
|
|
1100
|
+
if (action === 'define') {
|
|
1101
|
+
if (!name || !definition)
|
|
1102
|
+
return fail('name and definition required');
|
|
1103
|
+
const escaped = definition.replace(/'/g, "'\\''");
|
|
1104
|
+
const result = await manager.exec(nodeId, `mkdir -p ${wfDir} && echo '${escaped}' > ${wfDir}/${name}.json`);
|
|
1105
|
+
return result.code === 0 ? okBrief(`workflow ${name} defined`) : fail(result.stderr);
|
|
1106
|
+
}
|
|
1107
|
+
if (action === 'run') {
|
|
1108
|
+
if (!name)
|
|
1109
|
+
return fail('name required');
|
|
1110
|
+
const readResult = await manager.exec(nodeId, `cat ${wfDir}/${name}.json 2>/dev/null`);
|
|
1111
|
+
if (readResult.code !== 0)
|
|
1112
|
+
return fail(`workflow ${name} not found`);
|
|
1113
|
+
let wf;
|
|
1114
|
+
try {
|
|
1115
|
+
wf = JSON.parse(readResult.stdout);
|
|
1116
|
+
}
|
|
1117
|
+
catch {
|
|
1118
|
+
return fail('invalid workflow definition');
|
|
1119
|
+
}
|
|
1120
|
+
const stepResults = [];
|
|
1121
|
+
const stepOutputs = [];
|
|
1122
|
+
for (let i = 0; i < wf.steps.length; i++) {
|
|
1123
|
+
const step = wf.steps[i];
|
|
1124
|
+
const stepNode = step.node ?? nodeId;
|
|
1125
|
+
let cmd = step.command
|
|
1126
|
+
.replace(/\{\{step(\d+)\}\}/g, (_, n) => stepOutputs[parseInt(n)] ?? '')
|
|
1127
|
+
.replace(/\{\{(\w+)\}\}/g, (_, key) => resultStore.get(key) ?? `{{${key}}}`);
|
|
1128
|
+
const result = await manager.exec(stepNode, cmd);
|
|
1129
|
+
stepOutputs[i] = result.stdout.trim();
|
|
1130
|
+
if (step.store_as && result.code === 0)
|
|
1131
|
+
resultStore.set(step.store_as, result.stdout.trim());
|
|
1132
|
+
const status = result.code === 0 ? 'ok' : `exit ${result.code}`;
|
|
1133
|
+
stepResults.push(useJson
|
|
1134
|
+
? JSON.stringify({ step: i, node: stepNode, label: step.label, ok: result.code === 0, code: result.code, ms: result.durationMs, stdout: result.stdout.slice(0, 1000) })
|
|
1135
|
+
: `[${i}] ${stepNode} > ${step.label ?? `step ${i}`} ${t(result.durationMs)} ${status}\n${result.stdout.split('\n').slice(0, 5).join('\n')}`);
|
|
1136
|
+
if (result.code !== 0)
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
return useJson ? okBrief(`[${stepResults.join(',')}]`) : okBrief(trim(stepResults.join('\n\n')));
|
|
1140
|
+
}
|
|
1141
|
+
if (action === 'list') {
|
|
1142
|
+
const result = await manager.exec(nodeId, `ls -1 ${wfDir}/*.json 2>/dev/null | xargs -I{} basename {} .json || echo '(no workflows)'`);
|
|
1143
|
+
return ok(nodeId, result.durationMs, result.stdout, 'workflows');
|
|
1144
|
+
}
|
|
1145
|
+
if (action === 'get') {
|
|
1146
|
+
if (!name)
|
|
1147
|
+
return fail('name required');
|
|
1148
|
+
const result = await manager.exec(nodeId, `cat ${wfDir}/${name}.json 2>/dev/null || echo 'not found'`);
|
|
1149
|
+
return ok(nodeId, result.durationMs, result.stdout, `workflow ${name}`);
|
|
1150
|
+
}
|
|
1151
|
+
if (action === 'delete') {
|
|
1152
|
+
if (!name)
|
|
1153
|
+
return fail('name required');
|
|
1154
|
+
await manager.exec(nodeId, `rm -f ${wfDir}/${name}.json`);
|
|
1155
|
+
return okBrief(`workflow ${name} deleted`);
|
|
1156
|
+
}
|
|
1157
|
+
return fail('invalid action');
|
|
1158
|
+
});
|
|
665
1159
|
return server;
|
|
666
1160
|
}
|
|
667
1161
|
//# sourceMappingURL=server.js.map
|