host-execution-mcp 1.0.1 → 2.0.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +13 -11
  3. package/SECURITY.md +3 -3
  4. package/dist/access/index.d.ts +21 -10
  5. package/dist/access/index.js +55 -27
  6. package/dist/access/index.js.map +1 -1
  7. package/dist/access/paradi-auth.d.ts +3 -3
  8. package/dist/access/paradi-auth.js +4 -4
  9. package/dist/access/paradi-auth.js.map +1 -1
  10. package/dist/access/paradi-transport.d.ts +1 -1
  11. package/dist/access/paradi-transport.js +3 -3
  12. package/dist/access/paradi-transport.js.map +1 -1
  13. package/dist/access/paradi-username.d.ts +1 -1
  14. package/dist/access/paradi-username.js +2 -2
  15. package/dist/access/paradi-username.js.map +1 -1
  16. package/dist/access/session-pool.d.ts +5 -0
  17. package/dist/access/session-pool.js +7 -0
  18. package/dist/access/session-pool.js.map +1 -1
  19. package/dist/access/ssh-transport.d.ts +1 -1
  20. package/dist/access/ssh-transport.js +3 -2
  21. package/dist/access/ssh-transport.js.map +1 -1
  22. package/dist/config-yaml.d.ts +54 -0
  23. package/dist/config-yaml.js +172 -0
  24. package/dist/config-yaml.js.map +1 -0
  25. package/dist/config.d.ts +45 -1
  26. package/dist/config.js +126 -22
  27. package/dist/config.js.map +1 -1
  28. package/dist/index.js +3 -3
  29. package/dist/index.js.map +1 -1
  30. package/dist/instructions.d.ts +4 -1
  31. package/dist/instructions.js +73 -51
  32. package/dist/instructions.js.map +1 -1
  33. package/dist/kubectl/runner.d.ts +7 -8
  34. package/dist/kubectl/runner.js +18 -21
  35. package/dist/kubectl/runner.js.map +1 -1
  36. package/dist/link-runtime.d.ts +39 -0
  37. package/dist/link-runtime.js +134 -0
  38. package/dist/link-runtime.js.map +1 -0
  39. package/dist/server.d.ts +2 -1
  40. package/dist/server.js +10 -12
  41. package/dist/server.js.map +1 -1
  42. package/dist/sources/guides.d.ts +34 -0
  43. package/dist/sources/guides.js +50 -0
  44. package/dist/sources/guides.js.map +1 -0
  45. package/dist/sources/index.d.ts +5 -0
  46. package/dist/sources/index.js +22 -0
  47. package/dist/sources/index.js.map +1 -0
  48. package/dist/sources/yaml-template.d.ts +31 -0
  49. package/dist/sources/yaml-template.js +35 -0
  50. package/dist/sources/yaml-template.js.map +1 -0
  51. package/dist/ssh/access-client.d.ts +6 -2
  52. package/dist/ssh/access-client.js +58 -27
  53. package/dist/ssh/access-client.js.map +1 -1
  54. package/dist/ssh/access-errors.js +1 -1
  55. package/dist/ssh/access-errors.js.map +1 -1
  56. package/dist/ssh/access-sftp.js +3 -3
  57. package/dist/ssh/access-sftp.js.map +1 -1
  58. package/dist/tools/copy-job-schemas.d.ts +96 -581
  59. package/dist/tools/copy-job-schemas.js +9 -9
  60. package/dist/tools/copy-job-schemas.js.map +1 -1
  61. package/dist/tools/register.d.ts +2 -10
  62. package/dist/tools/register.js +174 -331
  63. package/dist/tools/register.js.map +1 -1
  64. package/dist/tools/remote-schemas.d.ts +156 -708
  65. package/dist/tools/remote-schemas.js +99 -103
  66. package/dist/tools/remote-schemas.js.map +1 -1
  67. package/dist/utils/copy-executor.d.ts +3 -1
  68. package/dist/utils/copy-executor.js +39 -25
  69. package/dist/utils/copy-executor.js.map +1 -1
  70. package/dist/utils/copy-transfer.d.ts +2 -0
  71. package/dist/utils/copy-transfer.js.map +1 -1
  72. package/dist/utils/remote-executor.d.ts +3 -4
  73. package/dist/utils/remote-executor.js +8 -18
  74. package/dist/utils/remote-executor.js.map +1 -1
  75. package/dist/utils/remote-target.js +1 -1
  76. package/dist/utils/remote-target.js.map +1 -1
  77. package/docs/AGENT_GUIDE.md +31 -103
  78. package/docs/CONFIGURATION.md +30 -9
  79. package/docs/GUIDE.md +186 -0
  80. package/docs/PARADI_ACCESS_PLAN.md +6 -6
  81. package/docs/TODO.md +4 -11
  82. package/docs/USER_GUIDE.zh-CN.md +26 -173
  83. package/docs/host-execution.yaml +92 -0
  84. package/package.json +3 -1
@@ -1,10 +1,9 @@
1
1
  import { z } from "zod";
2
- import { assertCopyEndpointsAllowed, executeRemoteCopy } from "../utils/copy-executor.js";
3
- import { CopyJobManager } from "../utils/copy-job-manager.js";
2
+ import { executeRemoteCopy } from "../utils/copy-executor.js";
4
3
  import { buildPsCommand, buildRemoteReadCommand, parseReadFileMeta, } from "../utils/remote-target.js";
5
4
  import { formatCommandResult, formatCopyFileResult, formatErrorResult, formatReadFileResult, toToolText, } from "../utils/result.js";
6
- import { remoteCopyCancelSchema, remoteCopyStatusSchema, buildRemoteCopyStartSchema, } from "./copy-job-schemas.js";
7
- import { buildRemoteCopyFileSchema, buildRemotePsSchema, buildRemoteReadFileSchema, fileEndpointToInternal, materializeCopyEndpoint, remoteInputToTarget, resolveReadOptions, } from "./remote-schemas.js";
5
+ import { remoteCopyCancelSchema, remoteCopyStatusSchema, remoteCopyStartSchema, } from "./copy-job-schemas.js";
6
+ import { assertCopySkipVerify, hopHostField, linkDescriptionField, materializeAgentCopyEndpoint, remoteCopyFileSchema, remotePsSchema, remoteReadFileSchema, resolveReadOptions, runTargetFromArgs, } from "./remote-schemas.js";
8
7
  async function handleTool(config, fn) {
9
8
  const started = Date.now();
10
9
  try {
@@ -21,15 +20,59 @@ async function handleTool(config, fn) {
21
20
  return toToolText(formatErrorResult(config, error, Date.now() - started));
22
21
  }
23
22
  }
24
- async function handleRemoteReadFile(config, remote, args) {
23
+ function openMachine(hub, pick) {
24
+ const session = hub.resolve({ description: pick.description });
25
+ const ctx = session.access.selectMachine({
26
+ host: pick.host,
27
+ user: pick.user,
28
+ password: pick.password,
29
+ });
30
+ return { session, contextKey: ctx.key };
31
+ }
32
+ /** Catch pick/select errors as tool JSON (same shape as handleTool), not MCP protocol faults. */
33
+ async function handleMachineTool(hub, pick, fn) {
25
34
  const started = Date.now();
26
- const resolved = resolveReadOptions(args);
27
35
  try {
36
+ const { session, contextKey } = openMachine(hub, pick);
37
+ return await handleTool(session.config, () => fn(session, contextKey));
38
+ }
39
+ catch (error) {
40
+ return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
41
+ }
42
+ }
43
+ function machineSelectFields(config) {
44
+ const targetUserConfigured = Boolean(config?.targetUser?.trim());
45
+ const targetPasswordConfigured = Boolean(config?.targetPassword);
46
+ const isParadi = config?.accessKind === "paradi-gateway";
47
+ return {
48
+ description: linkDescriptionField,
49
+ host: hopHostField,
50
+ user: z
51
+ .string()
52
+ .optional()
53
+ .describe(targetUserConfigured
54
+ ? `Omit to use TARGET_USER=${config?.targetUser}`
55
+ : "Required for a hop-2 / Paradi host unless the selected link has TARGET_USER"),
56
+ password: z
57
+ .string()
58
+ .optional()
59
+ .describe(isParadi
60
+ ? "Ignored for Paradi default auth (gateway password is ACCESS_PASSWORD)"
61
+ : targetPasswordConfigured
62
+ ? "Omit to use TARGET_PASSWORD"
63
+ : "Hop-2 password when the selected link has no TARGET_PASSWORD"),
64
+ };
65
+ }
66
+ async function handleRemoteReadFile(hub, args) {
67
+ const started = Date.now();
68
+ try {
69
+ const { session, contextKey } = openMachine(hub, args);
70
+ const resolved = resolveReadOptions(args);
28
71
  const command = buildRemoteReadCommand(args.path, resolved);
29
- const raw = await remote.runOnTarget(remoteInputToTarget(args), command, args.timeoutMs);
72
+ const raw = await session.remote.runOnTarget(runTargetFromArgs(contextKey, args), command, args.timeoutMs);
30
73
  const mergedStdout = [raw.stdout, raw.stderr].filter(Boolean).join("\n");
31
74
  const { meta, content } = parseReadFileMeta(mergedStdout);
32
- return toToolText(formatReadFileResult(config, {
75
+ return toToolText(formatReadFileResult(session.config, {
33
76
  content,
34
77
  exitCode: raw.exitCode,
35
78
  durationMs: raw.durationMs ?? Date.now() - started,
@@ -39,168 +82,90 @@ async function handleRemoteReadFile(config, remote, args) {
39
82
  }));
40
83
  }
41
84
  catch (error) {
42
- return toToolText(formatErrorResult(config, error, Date.now() - started));
85
+ return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
43
86
  }
44
87
  }
45
- function resolveCopyEndpoints(access, config, args) {
46
- const sourceRaw = fileEndpointToInternal(args.source);
47
- const destRaw = fileEndpointToInternal(args.dest);
48
- assertCopyEndpointsAllowed(config, sourceRaw, destRaw);
49
- const source = materializeCopyEndpoint(access, sourceRaw);
50
- const dest = materializeCopyEndpoint(access, destRaw);
51
- return { source, dest };
88
+ function resolveCopyEndpoints(hub, args) {
89
+ assertCopySkipVerify(args);
90
+ const { session, contextKey } = openMachine(hub, args);
91
+ return {
92
+ session,
93
+ source: materializeAgentCopyEndpoint(session.access, contextKey, args.source),
94
+ dest: materializeAgentCopyEndpoint(session.access, contextKey, args.dest),
95
+ };
96
+ }
97
+ function sessionlessConfig() {
98
+ return {
99
+ accessKind: "ssh",
100
+ accessHost: "",
101
+ accessUser: "",
102
+ accessPort: 22,
103
+ defaultTimeoutMs: 60_000,
104
+ maxOutputChars: 65_536,
105
+ };
52
106
  }
53
107
  export function registerTools(server, deps) {
54
- const { config, accessClient, access, kubectl, remote } = deps;
55
- const copyJobs = new CopyJobManager({ accessClient, config });
56
- const targetUserConfigured = Boolean(config.targetUser?.trim());
57
- const targetPasswordConfigured = Boolean(config.targetPassword);
58
- const targetHostConfigured = Boolean(config.targetHost?.trim());
59
- const isParadi = config.accessKind === "paradi-gateway";
60
- const allowContext = !isParadi;
61
- const remoteReadFileSchema = buildRemoteReadFileSchema({ allowContext });
62
- const remotePsSchema = buildRemotePsSchema({ allowContext });
63
- const remoteCopyFileSchema = buildRemoteCopyFileSchema({ allowContext });
64
- const remoteCopyStartSchema = buildRemoteCopyStartSchema({ allowContext });
65
- const contextKeyField = allowContext
66
- ? z
67
- .string()
68
- .optional()
69
- .describe("HostExecutionContext key from context_derive (omit for landing Context)")
70
- : undefined;
71
- const shellInputSchema = {
72
- command: z.string().min(1).describe("Shell command to run on the selected Context"),
73
- timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
74
- cwd: z.string().optional().describe("Working directory on the Context"),
75
- ...(contextKeyField ? { contextKey: contextKeyField } : {}),
76
- };
108
+ const { hub } = deps;
109
+ const config = hub.hintConfig();
110
+ const select = machineSelectFields(config);
77
111
  server.registerTool("access_check", {
78
- description: isParadi
79
- ? `Probe Local → Paradi gateway → TARGET connectivity (≤10s). Structured JSON: reachable, latencyMs, host, port, auth.
112
+ description: `Probe Local → ACCESS SSH connectivity with short timeouts (≤10s). Structured JSON: reachable, latencyMs, host, port, auth.
80
113
 
81
- Under ACCESS_KIND=paradi-gateway, AccessTransport SSHs the gateway with encoded username; success means landing Context (TARGET) is reachable. Does NOT run kubectl.
114
+ AccessTransport only (MCP client → SSH → ACCESS). Does NOT run kubectl or touch hop-2 hosts. Uses an ephemeral SSH session.
82
115
 
83
116
  When to use: first session; VPN/network unsure; after ACCESS_UNAVAILABLE.
84
- If reachable=false: stop; check VPN / ACCESS_HOST (gateway). Example: {}`
85
- : `Probe Local → ACCESS SSH connectivity with short timeouts (≤10s). Structured JSON: reachable, latencyMs, host, port, auth.
86
-
87
- AccessTransport only (MCP client → SSH → ACCESS). Does NOT run kubectl or touch pods/hosts. Uses an ephemeral SSH session (does not sever in-flight shell_exec / copy).
88
-
89
- When to use: first session; VPN/network unsure; after ACCESS_UNAVAILABLE; before long copy/exec when connectivity may be down.
90
- When NOT to use: checking cluster/kubectl health (shell_exec cluster-info); pod/context ops.
91
- If reachable=false (code ACCESS_UNAVAILABLE): stop retrying other tools; tell user to check VPN / ACCESS_HOST (hints in message). Negative TTL may return a cached failure without re-probing.
92
- Example: {}`,
93
- inputSchema: {},
94
- }, async () => {
117
+ When NOT to use: cluster/kubectl health (shell_exec cluster-info); pod ops.
118
+ If reachable=false (code ACCESS_UNAVAILABLE): stop THIS YAML link (VPN / ACCESS_HOST). Other descriptions may still work. Negative TTL may return a cached failure without re-probing.
119
+ Example: {} or {"description":"<exact link>"}`,
120
+ inputSchema: {
121
+ description: linkDescriptionField,
122
+ },
123
+ }, async ({ description }) => {
95
124
  const started = Date.now();
96
125
  try {
97
- const result = await accessClient.check();
126
+ const session = hub.resolve({ description });
127
+ const result = await session.accessClient.check();
98
128
  return toToolText({ ...result });
99
129
  }
100
130
  catch (error) {
101
- return toToolText(formatErrorResult(config, error, Date.now() - started));
131
+ return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
102
132
  }
103
133
  });
104
134
  server.registerTool("shell_exec", {
105
- description: isParadi
106
- ? `Run a shell command on the Paradi landing Context (TARGET host OS).
107
-
108
- Architecture: MCP → SSH(gateway, encoded user) → TARGET shell. Omit contextKey.
135
+ description: `Run a shell command on the selected machine.
109
136
 
110
- When to use: TARGET OS shell, kubectl on TARGET, local scripts on TARGET.
111
- When NOT to use: second-hop SSH (context_derive / ssh_exec unsupported under Paradi); inside a pod (k8s_exec).
112
- Example: {"command":"hostname","timeoutMs":30000}`
113
- : `Run a shell command on a HostExecutionContext (default landing).
114
-
115
- Architecture: MCP client → SSH → ACCESS → shell on landing or derived Context (contextKey).
116
-
117
- When to use: kubectl cluster-info, landing scripts, or shell on a derived host via contextKey.
118
- When NOT to use: inside a pod (k8s_exec), derive-only probe (context_derive), pure connectivity probe (access_check).
119
- Prerequisites: prefer access_check if VPN/network may be down. On ACCESS_UNAVAILABLE: do not retry — ask user to fix VPN.
120
- Example: {"command":"kubectl get nodes","timeoutMs":30000} or {"command":"hostname","contextKey":"<from context_derive>"}`,
121
- inputSchema: shellInputSchema,
122
- }, async ({ command, contextKey, timeoutMs, cwd }) => handleTool(config, () => access.shellExec(command, {
137
+ Omit host = this YAML link's landing (laptop-direct host, or the jump). Pass host = ssh hop-2 from ACCESS, or Paradi TARGET select.
138
+ When NOT to use: inside a pod (k8s_exec), connectivity probe (access_check).
139
+ Example: {"command":"hostname"} or {"host":"10.0.0.5","command":"hostname"}`,
140
+ inputSchema: {
141
+ command: z.string().min(1).describe("Shell command to run on the selected machine"),
142
+ timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
143
+ cwd: z.string().optional().describe("Working directory"),
144
+ ...select,
145
+ },
146
+ }, async ({ command, timeoutMs, cwd, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.access.shellExec(command, {
123
147
  contextKey,
124
- timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
148
+ timeoutMs: timeoutMs ?? session.config.defaultTimeoutMs,
125
149
  cwd,
126
150
  })));
127
- server.registerTool("context_derive", {
128
- description: isParadi
129
- ? `UNAVAILABLE under ACCESS_KIND=paradi-gateway — landing Context is already TARGET. Handler throws.`
130
- : `Derive a child HostExecutionContext from landing. Returns contextKey + endpoint + capabilities summary.
131
-
132
- Use the returned contextKey on shell_exec, docker_*, context_probe, remote_* target=context, and remote_copy_* target=context.
133
- Omit host to use TARGET_HOST when configured; useTarget=true forces default TARGET derive selector.
134
- Example: {"host":"10.0.0.5","user":"admin"} or {"useTarget":true}`,
135
- inputSchema: isParadi
136
- ? {}
137
- : {
138
- host: z
139
- .string()
140
- .optional()
141
- .describe(targetHostConfigured
142
- ? `Omit to use TARGET_HOST=${config.targetHost} with useTarget=true`
143
- : "Target host IP/hostname reachable FROM ACCESS"),
144
- useTarget: z
145
- .boolean()
146
- .optional()
147
- .describe(targetHostConfigured
148
- ? `When true, derive TARGET_HOST=${config.targetHost}`
149
- : "When true, derive default TARGET from config (requires TARGET_HOST)"),
150
- user: z
151
- .string()
152
- .optional()
153
- .describe(targetUserConfigured
154
- ? `Omit to use TARGET_USER=${config.targetUser}`
155
- : "SSH user (required unless TARGET_USER set)"),
156
- password: z
157
- .string()
158
- .optional()
159
- .describe(targetPasswordConfigured
160
- ? "Omit to use TARGET_PASSWORD"
161
- : "SSH password via sshpass (or set TARGET_PASSWORD)"),
162
- },
163
- }, async (args) => {
164
- const started = Date.now();
165
- try {
166
- const summary = access.deriveContext(args);
167
- return toToolText({
168
- success: true,
169
- ...summary,
170
- durationMs: Date.now() - started,
171
- });
172
- }
173
- catch (error) {
174
- return toToolText(formatErrorResult(config, error, Date.now() - started));
175
- }
176
- });
177
151
  server.registerTool("k8s_list_pods", {
178
- description: `List pods in a namespace (kubectl on landing Context only — ${isParadi ? "TARGET" : "ACCESS"}).
179
-
180
- By design k8s_* have no contextKey. For kubectl on a derived host use shell_exec with contextKey.
181
-
182
- No capabilities.kubectl gate — kubectl failures surface via exitCode; use context_probe to check kubectl CLI presence.
152
+ description: `List pods in a namespace (kubectl get pods on the selected machine).
183
153
 
184
- When to use: discover pod names before k8s_exec, k8s_logs, or remote_* with target=pod.
185
- When NOT to use: running commands in a pod (k8s_exec), reading files (remote_read_file), listing processes (remote_ps), SSH to hosts (ssh_exec).
186
- Prerequisites: namespace from user or context.
187
- Example: {"namespace":"default","labelSelector":"app=web","fieldSelector":"status.phase=Running","output":"wide"}`,
154
+ No capabilities.kubectl gate — failures surface via exitCode; use context_probe for CLI presence.
155
+ Example: {"namespace":"default","labelSelector":"app=web","output":"wide"}`,
188
156
  inputSchema: {
189
157
  namespace: z.string().min(1),
190
158
  labelSelector: z.string().optional(),
191
159
  fieldSelector: z.string().optional(),
192
160
  output: z.enum(["wide", "json", "name"]).optional(),
193
161
  timeoutMs: z.number().int().positive().optional(),
162
+ ...select,
194
163
  },
195
- }, async ({ namespace, labelSelector, fieldSelector, output, timeoutMs }) => handleTool(config, () => kubectl.listPods(namespace, { labelSelector, fieldSelector, output }, timeoutMs)));
164
+ }, async ({ namespace, labelSelector, fieldSelector, output, timeoutMs, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.kubectl.listPods(namespace, { labelSelector, fieldSelector, output }, timeoutMs, contextKey)));
196
165
  server.registerTool("k8s_exec", {
197
- description: `Run a command inside a pod container (${isParadi ? "TARGET" : "ACCESS"} → kubectl exec, non-interactive).
166
+ description: `Run a command inside a pod container (kubectl exec on the selected machine, non-interactive).
198
167
 
199
- Architecture: MCP client → SSH → ${isParadi ? "TARGET" : "ACCESS"} → kubectl exec → pod. NOT local SSH to pod.
200
-
201
- When to use: run scripts, inspect env, list dirs, start processes inside a container.
202
- When NOT to use: file logs on disk (remote_read_file mode=tail), container stdout history (k8s_logs), host OS via SSH (ssh_exec).
203
- Prerequisites: namespace and pod — use k8s_list_pods first if unknown.
168
+ When to use: scripts, env, dirs inside a container. Not file logs (remote_read_file) or container stdout history (k8s_logs).
204
169
  Example: {"namespace":"dev","pod":"web-abc","command":"ls -la /app","container":"app"}`,
205
170
  inputSchema: {
206
171
  namespace: z.string().min(1),
@@ -208,14 +173,13 @@ Example: {"namespace":"dev","pod":"web-abc","command":"ls -la /app","container":
208
173
  command: z.string().min(1),
209
174
  container: z.string().optional(),
210
175
  timeoutMs: z.number().int().positive().optional(),
176
+ ...select,
211
177
  },
212
- }, async ({ namespace, pod, command, container, timeoutMs }) => handleTool(config, () => kubectl.exec(namespace, pod, command, container, timeoutMs)));
178
+ }, async ({ namespace, pod, command, container, timeoutMs, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.kubectl.exec(namespace, pod, command, container, timeoutMs, contextKey)));
213
179
  server.registerTool("k8s_logs", {
214
- description: `Fetch pod container stdout/stderr (${isParadi ? "TARGET" : "ACCESS"} → kubectl logs).
180
+ description: `Fetch pod container stdout/stderr (kubectl logs on the selected machine).
215
181
 
216
- When to use: process output captured by container runtime logging.
217
- When NOT to use: log files on disk (remote_read_file mode=tail). Often empty for sshd-only idle pods.
218
- Prerequisites: namespace and pod name.
182
+ When to use: container runtime logs. Not log files on disk (remote_read_file). Often empty for sshd-only idle pods.
219
183
  Example: {"namespace":"dev","pod":"web-abc","tail":200,"since":"1h"}`,
220
184
  inputSchema: {
221
185
  namespace: z.string().min(1),
@@ -224,97 +188,28 @@ Example: {"namespace":"dev","pod":"web-abc","tail":200,"since":"1h"}`,
224
188
  tail: z.number().int().positive().optional(),
225
189
  since: z.string().optional(),
226
190
  timeoutMs: z.number().int().positive().optional(),
191
+ ...select,
227
192
  },
228
- }, async ({ namespace, pod, container, tail, since, timeoutMs }) => handleTool(config, () => kubectl.logs(namespace, pod, { container, tail, since }, timeoutMs)));
229
- server.registerTool("ssh_exec", {
230
- description: isParadi
231
- ? `UNAVAILABLE under ACCESS_KIND=paradi-gateway — landing Context is already TARGET. Use shell_exec for TARGET shell, docker_* (omit contextKey), or k8s_* for pods. Handler errors if called.`
232
- : `Shortcut: context_derive + shell_exec on a remote host (ACCESS → ssh/sshpass → host).
233
-
234
- IMPORTANT: NOT SSH from the laptop. MCP → ACCESS; ACCESS derives and shells to the host.
235
- Returns reusable contextKey + standard command result fields (exitCode, stdout, etc.). Prefer this for derive+shell in one call; reuse contextKey for follow-up shell_exec / docker_* / remote_* target=context.
236
-
237
- When to use: host IP/node when kubectl exec is not appropriate; omit host to use TARGET_HOST when configured.
238
- When NOT to use: inside a pod (k8s_exec); ACCESS_KIND=paradi-gateway (unsupported — handler throws).
239
- Example: {"host":"10.0.0.5","command":"hostname"} or {"command":"hostname"} with TARGET_HOST set`,
240
- inputSchema: {
241
- host: z
242
- .string()
243
- .optional()
244
- .describe(isParadi
245
- ? "UNAVAILABLE under paradi-gateway"
246
- : targetHostConfigured
247
- ? `Omit to use TARGET_HOST=${config.targetHost}`
248
- : "Target host IP/hostname reachable FROM ACCESS (required unless TARGET_HOST set)"),
249
- command: z.string().min(1),
250
- user: z
251
- .string()
252
- .optional()
253
- .describe(isParadi
254
- ? "UNAVAILABLE under paradi-gateway"
255
- : targetUserConfigured
256
- ? `Omit to use TARGET_USER=${config.targetUser}`
257
- : "SSH user (required unless TARGET_USER set)"),
258
- password: z
259
- .string()
260
- .optional()
261
- .describe(isParadi
262
- ? "UNAVAILABLE under paradi-gateway"
263
- : targetPasswordConfigured
264
- ? "Omit to use TARGET_PASSWORD"
265
- : "SSH password via sshpass (or set TARGET_PASSWORD)"),
266
- timeoutMs: z.number().int().positive().optional(),
267
- },
268
- }, async ({ host, command, user, password, timeoutMs }) => {
269
- const started = Date.now();
270
- try {
271
- const raw = await access.sshExec({
272
- host,
273
- command,
274
- user,
275
- password,
276
- timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
277
- });
278
- const mergedStdout = [raw.stdout, raw.stderr].filter(Boolean).join("\n");
279
- const base = formatCommandResult(config, {
280
- stdout: mergedStdout,
281
- stderr: "",
282
- exitCode: raw.exitCode,
283
- durationMs: raw.durationMs ?? Date.now() - started,
284
- });
285
- return toToolText({
286
- ...base,
287
- contextKey: raw.contextKey,
288
- endpoint: raw.endpoint,
289
- });
290
- }
291
- catch (error) {
292
- return toToolText(formatErrorResult(config, error, Date.now() - started));
293
- }
294
- });
295
- const withOptionalContextKey = (fields) => contextKeyField ? { ...fields, contextKey: contextKeyField } : fields;
193
+ }, async ({ namespace, pod, container, tail, since, timeoutMs, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.kubectl.logs(namespace, pod, { container, tail, since }, timeoutMs, contextKey)));
296
194
  server.registerTool("context_probe", {
297
- description: isParadi
298
- ? `Probe TARGET landing capabilities (docker/kubectl CLI). SSH success ≠ docker/kubectl.
299
- Example: {}`
300
- : `Probe Context capabilities (docker/kubectl CLI presence). SSH success does NOT imply docker/kubectl.
195
+ description: `Probe docker/kubectl CLI on the selected machine. SSH success ≠ docker/kubectl.
301
196
 
302
- Returns capabilities tri-state: true | false | unknown. Prefer this (or let docker_* auto-probe) before assuming docker.
303
- Default Context = landing; pass contextKey to probe a derived host.
304
- Example: {} or {"contextKey":"<from context_derive>"}`,
305
- inputSchema: withOptionalContextKey({
197
+ Returns true | false | unknown. Prefer this (or let docker_* auto-probe) before assuming docker.
198
+ Example: {} or {"host":"10.0.0.5"}`,
199
+ inputSchema: {
306
200
  timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
307
- }),
308
- }, async ({ contextKey, timeoutMs }) => {
201
+ ...select,
202
+ },
203
+ }, async ({ timeoutMs, description, host, user, password }) => {
309
204
  const started = Date.now();
310
205
  try {
311
- const result = await access.probe({
206
+ const { session, contextKey } = openMachine(hub, { description, host, user, password });
207
+ const result = await session.access.probe({
312
208
  contextKey,
313
- timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
209
+ timeoutMs: timeoutMs ?? session.config.defaultTimeoutMs,
314
210
  });
315
211
  return toToolText({
316
212
  success: true,
317
- contextKey: result.contextKey,
318
213
  endpoint: result.endpoint,
319
214
  capabilities: result.capabilities,
320
215
  durationMs: Date.now() - started,
@@ -322,120 +217,81 @@ Example: {} or {"contextKey":"<from context_derive>"}`,
322
217
  });
323
218
  }
324
219
  catch (error) {
325
- return toToolText(formatErrorResult(config, error, Date.now() - started));
220
+ return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
326
221
  }
327
222
  });
328
223
  server.registerTool("docker_ps", {
329
- description: isParadi
330
- ? `List Docker containers on TARGET landing (Runtime via SessionPool). Auto-probes docker.
331
- Example: {"all":true}`
332
- : `List Docker containers on a HostExecutionContext (landing or derived via contextKey).
333
-
334
- Runtime only — uses SessionPool; does not create SSH. Auto-probes capabilities.docker if unknown; fails if false.
335
- Example: {"all":true} or {"contextKey":"<from context_derive>","all":true}`,
336
- inputSchema: withOptionalContextKey({
224
+ description: `List Docker containers on the selected machine (auto-probes docker).
225
+ Example: {"all":true} or {"host":"10.0.0.5","all":true}`,
226
+ inputSchema: {
337
227
  timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
338
228
  all: z.boolean().optional().describe("Include stopped containers (-a)"),
339
229
  quiet: z.boolean().optional().describe("Only container IDs (-q)"),
340
230
  filter: z.string().optional().describe("docker ps --filter value"),
341
- }),
342
- }, async ({ contextKey, timeoutMs, all, quiet, filter, }) => handleTool(config, () => access.dockerPs({
231
+ ...select,
232
+ },
233
+ }, async ({ timeoutMs, all, quiet, filter, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.access.dockerPs({
343
234
  contextKey,
344
235
  all,
345
236
  quiet,
346
237
  filter,
347
- timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
238
+ timeoutMs: timeoutMs ?? session.config.defaultTimeoutMs,
348
239
  })));
349
240
  server.registerTool("docker_exec", {
350
- description: isParadi
351
- ? `Run a command inside a Docker container on TARGET landing (docker exec → bash -lc). Not kubectl exec. Auto-probes docker.
352
- Example: {"container":"musa-starvla","command":"nvidia-smi"}`
353
- : `Run a command inside a Docker container on landing or derived Context (docker exec → bash -lc).
354
-
355
- Not kubectl exec (use k8s_exec for pods). Auto-probes docker capability.
356
- Example: {"container":"musa-starvla","command":"nvidia-smi"} or {"contextKey":"<key>","container":"c1","command":"pwd"}`,
357
- inputSchema: withOptionalContextKey({
241
+ description: `Run a command inside a Docker container (docker exec → bash -lc). Not kubectl exec.
242
+ Example: {"container":"app","command":"pwd"} or {"host":"10.0.0.5","container":"c1","command":"pwd"}`,
243
+ inputSchema: {
358
244
  container: z.string().min(1).describe("Container name or ID"),
359
245
  command: z.string().min(1).describe("Command run via bash -lc inside the container"),
360
246
  workdir: z.string().optional().describe("docker exec -w"),
361
247
  timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
362
- }),
363
- }, async ({ container, command, workdir, contextKey, timeoutMs, }) => handleTool(config, () => access.dockerExec({
248
+ ...select,
249
+ },
250
+ }, async ({ container, command, workdir, timeoutMs, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.access.dockerExec({
364
251
  container,
365
252
  command,
366
253
  workdir,
367
254
  contextKey,
368
- timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
255
+ timeoutMs: timeoutMs ?? session.config.defaultTimeoutMs,
369
256
  })));
370
257
  server.registerTool("docker_logs", {
371
- description: isParadi
372
- ? `Fetch Docker container logs on TARGET landing (docker logs). For k8s pod logs use k8s_logs. Auto-probes docker.
373
- Example: {"container":"musa-starvla","tail":200}`
374
- : `Fetch Docker container logs (docker logs) on landing or derived Context.
375
-
376
- For k8s pod logs use k8s_logs. Auto-probes docker capability.
377
- Example: {"container":"musa-starvla","tail":200}`,
378
- inputSchema: withOptionalContextKey({
258
+ description: `Fetch Docker container logs on the selected machine. For k8s pod logs use k8s_logs.
259
+ Example: {"container":"app","tail":200}`,
260
+ inputSchema: {
379
261
  container: z.string().min(1),
380
262
  tail: z.number().int().positive().optional().describe("Default 200"),
381
263
  since: z.string().optional().describe("docker logs --since"),
382
264
  timestamps: z.boolean().optional(),
383
265
  timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds"),
384
- }),
385
- }, async ({ container, tail, since, timestamps, contextKey, timeoutMs, }) => handleTool(config, () => access.dockerLogs({
266
+ ...select,
267
+ },
268
+ }, async ({ container, tail, since, timestamps, timeoutMs, description, host, user, password }) => handleMachineTool(hub, { description, host, user, password }, (session, contextKey) => session.access.dockerLogs({
386
269
  container,
387
270
  tail,
388
271
  since,
389
272
  timestamps,
390
273
  contextKey,
391
- timeoutMs: timeoutMs ?? config.defaultTimeoutMs,
274
+ timeoutMs: timeoutMs ?? session.config.defaultTimeoutMs,
392
275
  })));
393
276
  server.registerTool("remote_read_file", {
394
- description: isParadi
395
- ? `Read a file on TARGET landing (target=landing) or in a pod (target=pod).
396
-
397
- Schema rejects target=context under Paradi — use target=landing for OS files.
398
- Use mode=head|tail. Limits: MAX_OUTPUT_CHARS. For transfer use remote_copy_file.
399
- Example: {"target":"landing","path":"/var/log/app.log","mode":"tail","lines":100}`
400
- : `Read a file on landing, in a pod, or on a derived Context (target=context).
401
-
402
- Use target to select path: landing=shell, pod=kubectl exec, context=derived shell (contextKey required).
403
- Use mode to read from start or end of file:
404
- - mode=head (default): first N lines; default lines=500; supports offset for pagination
405
- - mode=tail: last N lines; default lines=200; supports grep filter
406
-
407
- Limits: MAX_OUTPUT_CHARS env (default 65536) may truncate response; check truncatedByChars.
408
- Response includes totalLines/totalBytes/truncatedByLines. For file transfer use remote_copy_file.
277
+ description: `Read a file on the selected machine (description + optional host). Add namespace+pod to read inside a container via kubectl on that machine.
409
278
 
279
+ mode=head (default, lines=500, offset) or mode=tail (lines=200, grep).
410
280
  When NOT to use: container stdout (k8s_logs); copying files (remote_copy_file).
411
- Example (head): {"target":"pod","namespace":"dev","pod":"web","path":"/app/main.py","mode":"head","lines":2000}
412
- Example (context): {"target":"context","contextKey":"<key>","path":"/etc/hostname"}`,
281
+ Example: {"path":"/etc/hostname"} or {"host":"10.0.0.5","path":"/var/log/app.log","mode":"tail"}`,
413
282
  inputSchema: remoteReadFileSchema,
414
- }, async (args) => handleRemoteReadFile(config, remote, args));
283
+ }, async (args) => handleRemoteReadFile(hub, args));
415
284
  server.registerTool("remote_copy_file", {
416
- description: isParadi
417
- ? `Synchronous copy between local, landing (=TARGET), or pod. Schema rejects target=context under Paradi.
418
-
419
- local↔landing uses SFTP (declared degraded — prefer smaller transfers / remote_copy_start; expect retries).
420
- Example: {"source":{"target":"local","path":"C:/tmp/a.bin"},"dest":{"target":"landing","path":"/tmp/a.bin"}}`
421
- : `Synchronous copy of a file or directory between local, landing, pod, or context.
422
-
423
- Always waits for completion and returns success/bytes/transferId — does NOT start a background job.
424
- - local↔landing: independent SFTP (ssh2-sftp-client); files and directories (directory = walk + per-file)
425
- - landing/pod/context (context materializes before copy): kubectl cp, scp, cp on landing
285
+ description: `Synchronous copy. Endpoints: target=local (MCP machine), target=remote (selected Linux), target=pod (kubectl on that Linux).
426
286
 
427
- skipIfComplete: gate strength = verify (default size=cheap; size+sha256=content). verify=none + skip is rejected. Verify fail → rewrite (not throw-only).
428
- Set totalTimeoutMs (or timeoutMs alias). Progress via notifications/progress when client provides progressToken.
429
-
430
- When to use: short/medium sync copies that fit the MCP call timeout.
431
- When NOT to use: long/large copies — call remote_copy_start (async) then remote_copy_status (size>256MB or ETA>2min). Do not expect remote_copy_file to auto-async.
432
- Example (file): {"source":{"target":"local","path":"C:/tmp/a.bin"},"dest":{"target":"landing","path":"/tmp/a.bin"}}
433
- Example (context): {"source":{"target":"context","contextKey":"<key>","path":"/data/a.bin"},"dest":{"target":"local","path":"C:/data/a.bin"}}`,
287
+ Select the Linux with description + optional host (same as shell_exec). Paradi: no hop-2.
288
+ Large/long copies: remote_copy_start, not this tool.
289
+ Example: {"source":{"target":"remote","path":"/etc/hostname"},"dest":{"target":"local","path":"C:/tmp/hostname"}}`,
434
290
  inputSchema: remoteCopyFileSchema,
435
291
  }, async (args, extra) => {
436
292
  const started = Date.now();
437
293
  try {
438
- const { source, dest } = resolveCopyEndpoints(access, config, args);
294
+ const { session, source, dest } = resolveCopyEndpoints(hub, args);
439
295
  const progressToken = extra._meta?.progressToken;
440
296
  const onProgress = (progress) => {
441
297
  if (progressToken === undefined) {
@@ -452,7 +308,7 @@ Example (context): {"source":{"target":"context","contextKey":"<key>","path":"/d
452
308
  })
453
309
  .catch(() => undefined);
454
310
  };
455
- const result = await executeRemoteCopy({ accessClient, config }, source, dest, {
311
+ const result = await executeRemoteCopy({ accessClient: session.accessClient, config: session.config }, source, dest, {
456
312
  recursive: args.recursive ?? true,
457
313
  timeoutMs: args.timeoutMs,
458
314
  skipIfComplete: args.skipIfComplete,
@@ -479,27 +335,20 @@ Example (context): {"source":{"target":"context","contextKey":"<key>","path":"/d
479
335
  }));
480
336
  }
481
337
  catch (error) {
482
- return toToolText(formatErrorResult(config, error, Date.now() - started));
338
+ return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
483
339
  }
484
340
  });
485
341
  server.registerTool("remote_copy_start", {
486
- description: isParadi
487
- ? `Start an async copy job and return transferId immediately. Same rules as remote_copy_file under Paradi: landing=TARGET; schema rejects target=context. SFTP degraded — prefer this for large transfers.
488
- Example: {"source":{"target":"landing","path":"/tmp/big.pt"},"dest":{"target":"local","path":"C:/data/big.pt"},"skipIfComplete":true,"totalTimeoutMs":7200000}`
489
- : `Start an async file/directory copy job (same path engine as remote_copy_file) and return immediately with transferId.
342
+ description: `Start an async copy job (same rules as remote_copy_file) and return transferId immediately.
490
343
 
491
- This is the async primitive — remote_copy_file never auto-promotes to a job.
492
- Job state: ~/.agent-compose/remote-copy/jobs/<transferId>.json
493
- Poll remote_copy_status; cancel with remote_copy_cancel.
494
- Prefer when size>256MB or ETA>2min. Supports skipIfComplete/verify/totalTimeoutMs.
495
- Terminal statuses: success|failed|cancelled|timeout.
496
- Example: {"source":{"target":"landing","path":"/tmp/big.pt"},"dest":{"target":"local","path":"C:/data/big.pt"},"skipIfComplete":true,"totalTimeoutMs":7200000}`,
344
+ Prefer when size>256MB or ETA>2min. Poll remote_copy_status; cancel with remote_copy_cancel.
345
+ Example: {"source":{"target":"remote","path":"/tmp/big.pt"},"dest":{"target":"local","path":"C:/data/big.pt"},"skipIfComplete":true,"totalTimeoutMs":7200000}`,
497
346
  inputSchema: remoteCopyStartSchema,
498
347
  }, async (args) => {
499
348
  const started = Date.now();
500
349
  try {
501
- const { source, dest } = resolveCopyEndpoints(access, config, args);
502
- const result = await copyJobs.start({
350
+ const { session, source, dest } = resolveCopyEndpoints(hub, args);
351
+ const result = await hub.copyStart(session, {
503
352
  source,
504
353
  dest,
505
354
  recursive: args.recursive,
@@ -512,7 +361,7 @@ Example: {"source":{"target":"landing","path":"/tmp/big.pt"},"dest":{"target":"l
512
361
  return toToolText(result);
513
362
  }
514
363
  catch (error) {
515
- return toToolText(formatErrorResult(config, error, Date.now() - started));
364
+ return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
516
365
  }
517
366
  });
518
367
  server.registerTool("remote_copy_status", {
@@ -524,47 +373,41 @@ Example: {"transferId":"copy-xxx"}`,
524
373
  }, async (args) => {
525
374
  const started = Date.now();
526
375
  try {
527
- const result = await copyJobs.status(args.transferId);
376
+ const result = await hub.copyStatus(args.transferId);
528
377
  return toToolText(result);
529
378
  }
530
379
  catch (error) {
531
- return toToolText(formatErrorResult(config, error, Date.now() - started));
380
+ return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
532
381
  }
533
382
  });
534
383
  server.registerTool("remote_copy_cancel", {
535
384
  description: `Cancel a running/queued async copy job by transferId.
536
385
 
537
- Aborts the in-process worker: local↔landing SFTP sessions end promptly; kubectl cp / scp on landing is best-effort (abort checked between hops).
386
+ Aborts the in-process worker: local↔remote SFTP sessions end promptly; kubectl cp / scp on landing is best-effort.
538
387
  Already-finished jobs are left unchanged (terminal status is fenced).
539
388
  Example: {"transferId":"copy-xxx"}`,
540
389
  inputSchema: remoteCopyCancelSchema,
541
390
  }, async (args) => {
542
391
  const started = Date.now();
543
392
  try {
544
- const result = await copyJobs.cancel(args.transferId);
393
+ const result = await hub.copyCancel(args.transferId);
545
394
  return toToolText(result);
546
395
  }
547
396
  catch (error) {
548
- return toToolText(formatErrorResult(config, error, Date.now() - started));
397
+ return toToolText(formatErrorResult(hub.hintConfig() ?? sessionlessConfig(), error, Date.now() - started));
549
398
  }
550
399
  });
551
400
  server.registerTool("remote_ps", {
552
- description: isParadi
553
- ? `List processes on TARGET landing (target=landing) or in a pod (target=pod).
554
- Example: {"target":"landing","pattern":"python"}`
555
- : `List processes on landing, in a pod, or on a derived Context (ps aux | grep).
556
-
557
- When NOT to use: reading log files (remote_read_file).
558
- Example (pod): {"target":"pod","namespace":"dev","pod":"train-0","pattern":"python"}
559
- Example (context): {"target":"context","contextKey":"<key>","pattern":"nginx"}`,
401
+ description: `List processes on the selected machine (description + optional host). Add namespace+pod to list inside a container.
402
+ Example: {"pattern":"sshd"} or {"host":"10.0.0.5","pattern":"python"}`,
560
403
  inputSchema: remotePsSchema,
561
- }, async (args) => handleTool(config, async () => {
404
+ }, async (args) => handleMachineTool(hub, args, async (session, contextKey) => {
562
405
  const command = buildPsCommand(args.pattern);
563
- return remote.runOnTarget(remoteInputToTarget(args), command, args.timeoutMs);
406
+ return session.remote.runOnTarget(runTargetFromArgs(contextKey, args), command, args.timeoutMs);
564
407
  }));
565
408
  }
566
409
  export function registerPrompts(server) {
567
- server.prompt("tool-selection", "Recommend the best host-execution-mcp tool for a scenario (all ops go via ACCESS landing Context)", {
410
+ server.prompt("tool-selection", "Recommend the best host-execution-mcp tool for a scenario (all ops go via ACCESS landing, optional host for hop-2)", {
568
411
  scenario: z.string().describe("Description of what the user wants to do remotely"),
569
412
  }, async ({ scenario }) => ({
570
413
  messages: [
@@ -573,11 +416,11 @@ export function registerPrompts(server) {
573
416
  content: {
574
417
  type: "text",
575
418
  text: `Analyze the user's remote operation scenario and respond with:
576
- 1. Recommended tool (access_check | context_derive | context_probe | shell_exec | k8s_list_pods | k8s_exec | ssh_exec | docker_ps | docker_exec | docker_logs | k8s_logs | remote_read_file | remote_copy_file | remote_copy_start | remote_copy_status | remote_copy_cancel | remote_ps)
577
- 2. Why this tool fits (remember: MCP → ACCESS → Context; ssh_exec is ACCESS SSH derive, NOT local SSH; large copies — remote_copy_start; VPN/network unsure — access_check first)
419
+ 1. Recommended tool (access_check | context_probe | shell_exec | k8s_list_pods | k8s_exec | docker_ps | docker_exec | docker_logs | k8s_logs | remote_read_file | remote_copy_file | remote_copy_start | remote_copy_status | remote_copy_cancel | remote_ps)
420
+ 2. Why this tool fits (MCP → ACCESS landing; pass host only for hop-2 / Paradi TARGET select; large copies — remote_copy_start; VPN/network unsure — access_check first)
578
421
  3. The first MCP tool call (JSON arguments only)
579
422
 
580
- If VPN/connectivity may be down, recommend access_check first. Use k8s_list_pods when pod name is unknown. Use context_derive before target=context or contextKey tools. Prefer k8s_exec over ssh_exec for pod/container access.
423
+ If VPN/connectivity may be down, recommend access_check first. Use k8s_list_pods when pod name is unknown. Prefer k8s_exec for pod/container access.
581
424
 
582
425
  Scenario: ${scenario}`,
583
426
  },
@@ -603,8 +446,8 @@ Log path: ${log_path ?? "(ask user if needed)"}
603
446
  Process pattern: ${pattern ?? "(optional)"}
604
447
 
605
448
  Steps:
606
- 1. Verify connectivity (access_check); if unreachable stop and report VPN/ACCESS_HOST
607
- 2. Verify cluster (shell_exec kubectl cluster-info)
449
+ 1. Verify connectivity (access_check); if this link is unreachable, stop that link and report VPN/ACCESS_HOST
450
+ 2. Verify cluster (shell_exec kubectl cluster-info) when kubectl is needed
608
451
  3. List pods if needed (k8s_list_pods)
609
452
  4. Read or tail files (remote_read_file); copy files (remote_copy_file or remote_copy_start for large)
610
453
  5. Check processes (remote_ps)