host-execution-mcp 1.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 (126) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/LICENSE +21 -0
  3. package/README.md +92 -0
  4. package/SECURITY.md +72 -0
  5. package/dist/access/docker-runtime.d.ts +38 -0
  6. package/dist/access/docker-runtime.js +61 -0
  7. package/dist/access/docker-runtime.js.map +1 -0
  8. package/dist/access/index.d.ts +110 -0
  9. package/dist/access/index.js +174 -0
  10. package/dist/access/index.js.map +1 -0
  11. package/dist/access/paradi-auth.d.ts +33 -0
  12. package/dist/access/paradi-auth.js +89 -0
  13. package/dist/access/paradi-auth.js.map +1 -0
  14. package/dist/access/paradi-transport.d.ts +10 -0
  15. package/dist/access/paradi-transport.js +39 -0
  16. package/dist/access/paradi-transport.js.map +1 -0
  17. package/dist/access/paradi-username.d.ts +5 -0
  18. package/dist/access/paradi-username.js +14 -0
  19. package/dist/access/paradi-username.js.map +1 -0
  20. package/dist/access/probe.d.ts +14 -0
  21. package/dist/access/probe.js +63 -0
  22. package/dist/access/probe.js.map +1 -0
  23. package/dist/access/session-pool.d.ts +25 -0
  24. package/dist/access/session-pool.js +46 -0
  25. package/dist/access/session-pool.js.map +1 -0
  26. package/dist/access/ssh-transport.d.ts +16 -0
  27. package/dist/access/ssh-transport.js +74 -0
  28. package/dist/access/ssh-transport.js.map +1 -0
  29. package/dist/access/types.d.ts +43 -0
  30. package/dist/access/types.js +21 -0
  31. package/dist/access/types.js.map +1 -0
  32. package/dist/config.d.ts +52 -0
  33. package/dist/config.js +179 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +19 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/instructions.d.ts +4 -0
  39. package/dist/instructions.js +93 -0
  40. package/dist/instructions.js.map +1 -0
  41. package/dist/kubectl/runner.d.ts +32 -0
  42. package/dist/kubectl/runner.js +79 -0
  43. package/dist/kubectl/runner.js.map +1 -0
  44. package/dist/server.d.ts +3 -0
  45. package/dist/server.js +24 -0
  46. package/dist/server.js.map +1 -0
  47. package/dist/ssh/access-client.d.ts +62 -0
  48. package/dist/ssh/access-client.js +470 -0
  49. package/dist/ssh/access-client.js.map +1 -0
  50. package/dist/ssh/access-errors.d.ts +53 -0
  51. package/dist/ssh/access-errors.js +180 -0
  52. package/dist/ssh/access-errors.js.map +1 -0
  53. package/dist/ssh/access-sftp.d.ts +14 -0
  54. package/dist/ssh/access-sftp.js +456 -0
  55. package/dist/ssh/access-sftp.js.map +1 -0
  56. package/dist/ssh/bastion-client.d.ts +60 -0
  57. package/dist/ssh/bastion-client.js +423 -0
  58. package/dist/ssh/bastion-client.js.map +1 -0
  59. package/dist/ssh/bastion-errors.d.ts +62 -0
  60. package/dist/ssh/bastion-errors.js +190 -0
  61. package/dist/ssh/bastion-errors.js.map +1 -0
  62. package/dist/ssh/hop1-sftp.d.ts +14 -0
  63. package/dist/ssh/hop1-sftp.js +437 -0
  64. package/dist/ssh/hop1-sftp.js.map +1 -0
  65. package/dist/tools/copy-job-schemas.d.ts +709 -0
  66. package/dist/tools/copy-job-schemas.js +48 -0
  67. package/dist/tools/copy-job-schemas.js.map +1 -0
  68. package/dist/tools/register.d.ts +14 -0
  69. package/dist/tools/register.js +619 -0
  70. package/dist/tools/register.js.map +1 -0
  71. package/dist/tools/remote-schemas.d.ts +879 -0
  72. package/dist/tools/remote-schemas.js +214 -0
  73. package/dist/tools/remote-schemas.js.map +1 -0
  74. package/dist/utils/copy-executor.d.ts +41 -0
  75. package/dist/utils/copy-executor.js +231 -0
  76. package/dist/utils/copy-executor.js.map +1 -0
  77. package/dist/utils/copy-job-manager.d.ts +34 -0
  78. package/dist/utils/copy-job-manager.js +400 -0
  79. package/dist/utils/copy-job-manager.js.map +1 -0
  80. package/dist/utils/copy-job-store.d.ts +18 -0
  81. package/dist/utils/copy-job-store.js +104 -0
  82. package/dist/utils/copy-job-store.js.map +1 -0
  83. package/dist/utils/copy-job-types.d.ts +133 -0
  84. package/dist/utils/copy-job-types.js +37 -0
  85. package/dist/utils/copy-job-types.js.map +1 -0
  86. package/dist/utils/copy-transfer.d.ts +59 -0
  87. package/dist/utils/copy-transfer.js +86 -0
  88. package/dist/utils/copy-transfer.js.map +1 -0
  89. package/dist/utils/remote-copy.d.ts +14 -0
  90. package/dist/utils/remote-copy.js +115 -0
  91. package/dist/utils/remote-copy.js.map +1 -0
  92. package/dist/utils/remote-executor.d.ts +15 -0
  93. package/dist/utils/remote-executor.js +34 -0
  94. package/dist/utils/remote-executor.js.map +1 -0
  95. package/dist/utils/remote-target.d.ts +22 -0
  96. package/dist/utils/remote-target.js +75 -0
  97. package/dist/utils/remote-target.js.map +1 -0
  98. package/dist/utils/result.d.ts +87 -0
  99. package/dist/utils/result.js +81 -0
  100. package/dist/utils/result.js.map +1 -0
  101. package/dist/utils/shell-quote.d.ts +22 -0
  102. package/dist/utils/shell-quote.js +55 -0
  103. package/dist/utils/shell-quote.js.map +1 -0
  104. package/dist/utils/timeout.d.ts +1 -0
  105. package/dist/utils/timeout.js +18 -0
  106. package/dist/utils/timeout.js.map +1 -0
  107. package/dist/utils/truncate.d.ts +5 -0
  108. package/dist/utils/truncate.js +12 -0
  109. package/dist/utils/truncate.js.map +1 -0
  110. package/dist/version.d.ts +1 -0
  111. package/dist/version.js +7 -0
  112. package/dist/version.js.map +1 -0
  113. package/docs/AGENT_GUIDE.md +161 -0
  114. package/docs/CONFIGURATION.md +130 -0
  115. package/docs/COPY_SPECIALIZED_TEST_CHECKLIST.md +9 -0
  116. package/docs/HOP1_E2E_CHECKLIST.md +128 -0
  117. package/docs/HOP1_E2E_RESULTS.json +647 -0
  118. package/docs/PARADI_ACCESS_PLAN.md +272 -0
  119. package/docs/REMOTE_COPY_JOB_API.md +92 -0
  120. package/docs/TODO.md +66 -0
  121. package/docs/USER_GUIDE.zh-CN.md +93 -0
  122. package/docs/clients/claude-desktop.json.example +12 -0
  123. package/docs/clients/cursor-npx.json.example +12 -0
  124. package/docs/clients/cursor.json.example +12 -0
  125. package/docs/clients/vscode.json.example +12 -0
  126. package/package.json +65 -0
@@ -0,0 +1,87 @@
1
+ import type { AppConfig } from "../config.js";
2
+ export interface CommandResult {
3
+ success: boolean;
4
+ stdout: string;
5
+ stderr: string;
6
+ exitCode: number;
7
+ truncated: boolean;
8
+ durationMs: number;
9
+ }
10
+ export interface ReadFileResult extends CommandResult {
11
+ mode?: "head" | "tail";
12
+ totalLines?: number;
13
+ totalBytes?: number;
14
+ returnedLines?: number;
15
+ lines?: number;
16
+ maxLines?: number;
17
+ truncatedByLines?: boolean;
18
+ truncatedByChars?: boolean;
19
+ }
20
+ export interface CopyFileResult {
21
+ success: boolean;
22
+ operation: "copy";
23
+ source: {
24
+ target: string;
25
+ path: string;
26
+ };
27
+ dest: {
28
+ target: string;
29
+ path: string;
30
+ };
31
+ bytes?: number;
32
+ totalBytes?: number;
33
+ skippedBytes?: number;
34
+ skipped?: boolean;
35
+ skipReason?: "size-match" | "hash-match";
36
+ transferId?: string;
37
+ verified?: boolean;
38
+ usedFastPath?: boolean;
39
+ message: string;
40
+ exitCode: number;
41
+ durationMs: number;
42
+ }
43
+ export declare function formatCommandResult(config: AppConfig, raw: {
44
+ stdout: string;
45
+ stderr: string;
46
+ exitCode: number;
47
+ durationMs: number;
48
+ }): CommandResult;
49
+ export declare function formatErrorResult(config: AppConfig, error: unknown, durationMs: number): CommandResult;
50
+ export declare function formatReadFileResult(config: AppConfig, raw: {
51
+ content: string;
52
+ exitCode: number;
53
+ durationMs: number;
54
+ maxLines: number;
55
+ mode?: "head" | "tail";
56
+ meta?: {
57
+ totalLines?: number;
58
+ totalBytes?: number;
59
+ };
60
+ }): ReadFileResult;
61
+ export declare function formatCopyFileResult(raw: {
62
+ source: {
63
+ target: string;
64
+ path: string;
65
+ };
66
+ dest: {
67
+ target: string;
68
+ path: string;
69
+ };
70
+ exitCode: number;
71
+ durationMs: number;
72
+ bytes?: number;
73
+ totalBytes?: number;
74
+ skippedBytes?: number;
75
+ skipped?: boolean;
76
+ skipReason?: "size-match" | "hash-match";
77
+ transferId?: string;
78
+ verified?: boolean;
79
+ usedFastPath?: boolean;
80
+ message: string;
81
+ }): CopyFileResult;
82
+ export declare function toToolText(result: CommandResult | ReadFileResult | CopyFileResult | Record<string, unknown>): {
83
+ content: Array<{
84
+ type: "text";
85
+ text: string;
86
+ }>;
87
+ };
@@ -0,0 +1,81 @@
1
+ import { isAccessUnavailableError } from "../ssh/access-errors.js";
2
+ import { truncateOutput } from "./truncate.js";
3
+ export function formatCommandResult(config, raw) {
4
+ const combined = [raw.stdout, raw.stderr].filter(Boolean).join(raw.stdout && raw.stderr ? "\n" : "");
5
+ const { text, truncated } = truncateOutput(combined, config.maxOutputChars);
6
+ return {
7
+ success: raw.exitCode === 0,
8
+ stdout: text,
9
+ stderr: "",
10
+ exitCode: raw.exitCode,
11
+ truncated,
12
+ durationMs: raw.durationMs,
13
+ };
14
+ }
15
+ export function formatErrorResult(config, error, durationMs) {
16
+ const message = isAccessUnavailableError(error)
17
+ ? JSON.stringify({
18
+ ...error.toJSON(),
19
+ // Keep human-readable multiline message for Agents that only read stderr.
20
+ message: error.message,
21
+ }, null, 2)
22
+ : error instanceof Error
23
+ ? error.message
24
+ : String(error);
25
+ const { text, truncated } = truncateOutput(message, config.maxOutputChars);
26
+ return {
27
+ success: false,
28
+ stdout: "",
29
+ stderr: text,
30
+ exitCode: 1,
31
+ truncated,
32
+ durationMs,
33
+ };
34
+ }
35
+ export function formatReadFileResult(config, raw) {
36
+ const base = formatCommandResult(config, {
37
+ stdout: raw.content,
38
+ stderr: "",
39
+ exitCode: raw.exitCode,
40
+ durationMs: raw.durationMs,
41
+ });
42
+ const returnedLines = raw.content ? raw.content.split("\n").length : 0;
43
+ const totalLines = raw.meta?.totalLines;
44
+ const truncatedByLines = totalLines !== undefined && totalLines > raw.maxLines && returnedLines >= raw.maxLines;
45
+ return {
46
+ ...base,
47
+ mode: raw.mode,
48
+ totalLines,
49
+ totalBytes: raw.meta?.totalBytes,
50
+ returnedLines,
51
+ lines: raw.maxLines,
52
+ maxLines: raw.maxLines,
53
+ truncatedByLines,
54
+ truncatedByChars: base.truncated,
55
+ };
56
+ }
57
+ export function formatCopyFileResult(raw) {
58
+ return {
59
+ success: raw.exitCode === 0,
60
+ operation: "copy",
61
+ source: raw.source,
62
+ dest: raw.dest,
63
+ bytes: raw.bytes,
64
+ totalBytes: raw.totalBytes,
65
+ skippedBytes: raw.skippedBytes,
66
+ skipped: raw.skipped,
67
+ skipReason: raw.skipReason,
68
+ transferId: raw.transferId,
69
+ verified: raw.verified,
70
+ usedFastPath: raw.usedFastPath,
71
+ message: raw.message,
72
+ exitCode: raw.exitCode,
73
+ durationMs: raw.durationMs,
74
+ };
75
+ }
76
+ export function toToolText(result) {
77
+ return {
78
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
79
+ };
80
+ }
81
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.js","sourceRoot":"","sources":["../../src/utils/result.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Escape a string for safe embedding inside single-quoted bash -lc '...'.
3
+ * Uses the pattern: '...'"'"'...' for embedded single quotes.
4
+ */
5
+ export declare function bashSingleQuote(value: string): string;
6
+ export declare function buildBashLcCommand(command: string, cwd?: string): string;
7
+ /**
8
+ * Shared -o flags for Hop2 host ssh/scp.
9
+ * Password auth omits BatchMode so sshpass can prompt-less password auth.
10
+ */
11
+ export declare function hostSshBaseOpts(password?: string): string;
12
+ /**
13
+ * Optional sshpass + ssh|scp + shared -o opts (no target / remote command).
14
+ * Used by buildSshRemoteCommand and scp helpers so opts do not fork.
15
+ */
16
+ export declare function buildHostTransportPrefix(binary: "ssh" | "scp", password?: string): string;
17
+ /**
18
+ * Bastion → host remote exec: ssh … with the entire `bash -lc '<cmd>'` as one
19
+ * argv so OpenSSH does not space-join and truncate multi-word commands.
20
+ */
21
+ export declare function buildSshRemoteCommand(user: string, host: string, command: string, password?: string): string;
22
+ export declare function shellEscapeArg(value: string): string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Escape a string for safe embedding inside single-quoted bash -lc '...'.
3
+ * Uses the pattern: '...'"'"'...' for embedded single quotes.
4
+ */
5
+ export function bashSingleQuote(value) {
6
+ if (!value.includes("'")) {
7
+ return `'${value}'`;
8
+ }
9
+ const escapedQuote = "'\"'\"'";
10
+ return `'${value.replace(/'/g, escapedQuote)}'`;
11
+ }
12
+ export function buildBashLcCommand(command, cwd) {
13
+ const quoted = bashSingleQuote(command);
14
+ if (cwd) {
15
+ return `cd ${bashSingleQuote(cwd)} && bash -lc ${quoted}`;
16
+ }
17
+ return `bash -lc ${quoted}`;
18
+ }
19
+ /**
20
+ * Shared -o flags for Hop2 host ssh/scp.
21
+ * Password auth omits BatchMode so sshpass can prompt-less password auth.
22
+ */
23
+ export function hostSshBaseOpts(password) {
24
+ if (password) {
25
+ return "-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10";
26
+ }
27
+ return "-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new";
28
+ }
29
+ /**
30
+ * Optional sshpass + ssh|scp + shared -o opts (no target / remote command).
31
+ * Used by buildSshRemoteCommand and scp helpers so opts do not fork.
32
+ */
33
+ export function buildHostTransportPrefix(binary, password) {
34
+ const opts = hostSshBaseOpts(password);
35
+ if (password) {
36
+ return `sshpass -p ${bashSingleQuote(password)} ${binary} ${opts}`;
37
+ }
38
+ return `${binary} ${opts}`;
39
+ }
40
+ /**
41
+ * Bastion → host remote exec: ssh … with the entire `bash -lc '<cmd>'` as one
42
+ * argv so OpenSSH does not space-join and truncate multi-word commands.
43
+ */
44
+ export function buildSshRemoteCommand(user, host, command, password) {
45
+ const target = `${user}@${host}`;
46
+ const remote = `bash -lc ${bashSingleQuote(command)}`;
47
+ return `${buildHostTransportPrefix("ssh", password)} ${bashSingleQuote(target)} ${bashSingleQuote(remote)}`;
48
+ }
49
+ export function shellEscapeArg(value) {
50
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) {
51
+ return value;
52
+ }
53
+ return bashSingleQuote(value);
54
+ }
55
+ //# sourceMappingURL=shell-quote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-quote.js","sourceRoot":"","sources":["../../src/utils/shell-quote.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,GAAG,CAAC;IACtB,CAAC;IACD,MAAM,YAAY,GAAG,SAAS,CAAC;IAC/B,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,GAAY;IAC9D,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,GAAG,EAAE,CAAC;QACR,OAAO,MAAM,eAAe,CAAC,GAAG,CAAC,gBAAgB,MAAM,EAAE,CAAC;IAC5D,CAAC;IACD,OAAO,YAAY,MAAM,EAAE,CAAC;AAC9B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,QAAiB;IAC/C,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,0DAA0D,CAAC;IACpE,CAAC;IACD,OAAO,2EAA2E,CAAC;AACrF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAqB,EAAE,QAAiB;IAC/E,MAAM,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,cAAc,eAAe,CAAC,QAAQ,CAAC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;IACrE,CAAC;IACD,OAAO,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;AAC7B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAY,EACZ,IAAY,EACZ,OAAe,EACf,QAAiB;IAEjB,MAAM,MAAM,GAAG,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;IACjC,MAAM,MAAM,GAAG,YAAY,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;IACtD,OAAO,GAAG,wBAAwB,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,eAAe,CAAC,MAAM,CAAC,IAAI,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;AAC9G,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,0BAA0B,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, onTimeout?: () => void): Promise<T>;
@@ -0,0 +1,18 @@
1
+ export function withTimeout(promise, timeoutMs, onTimeout) {
2
+ return new Promise((resolve, reject) => {
3
+ const timer = setTimeout(() => {
4
+ onTimeout?.();
5
+ reject(new Error(`Operation timed out after ${timeoutMs}ms`));
6
+ }, timeoutMs);
7
+ promise
8
+ .then((value) => {
9
+ clearTimeout(timer);
10
+ resolve(value);
11
+ })
12
+ .catch((error) => {
13
+ clearTimeout(timer);
14
+ reject(error);
15
+ });
16
+ });
17
+ }
18
+ //# sourceMappingURL=timeout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../src/utils/timeout.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CACzB,OAAmB,EACnB,SAAiB,EACjB,SAAsB;IAEtB,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,SAAS,EAAE,EAAE,CAAC;YACd,MAAM,CAAC,IAAI,KAAK,CAAC,6BAA6B,SAAS,IAAI,CAAC,CAAC,CAAC;QAChE,CAAC,EAAE,SAAS,CAAC,CAAC;QAEd,OAAO;aACJ,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;YACd,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACxB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,5 @@
1
+ export interface TruncateResult {
2
+ text: string;
3
+ truncated: boolean;
4
+ }
5
+ export declare function truncateOutput(text: string, maxChars: number): TruncateResult;
@@ -0,0 +1,12 @@
1
+ export function truncateOutput(text, maxChars) {
2
+ if (text.length <= maxChars) {
3
+ return { text, truncated: false };
4
+ }
5
+ const suffix = `\n\n...[truncated, ${text.length - maxChars} chars omitted]`;
6
+ const keep = Math.max(0, maxChars - suffix.length);
7
+ return {
8
+ text: text.slice(0, keep) + suffix,
9
+ truncated: true,
10
+ };
11
+ }
12
+ //# sourceMappingURL=truncate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,QAAgB;IAC3D,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACpC,CAAC;IACD,MAAM,MAAM,GAAG,sBAAsB,IAAI,CAAC,MAAM,GAAG,QAAQ,iBAAiB,CAAC;IAC7E,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACnD,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,MAAM;QAClC,SAAS,EAAE,IAAI;KAChB,CAAC;AACJ,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const VERSION: string;
@@ -0,0 +1,7 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const packagePath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
5
+ const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
6
+ export const VERSION = pkg.version;
7
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;AACxF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAwB,CAAC;AAEjF,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC"}
@@ -0,0 +1,161 @@
1
+ # Agent Guide — host-execution-mcp
2
+
3
+ Extended reference for LLM agents. A shorter version is injected as MCP server `instructions` at handshake (KIND-aware for `ssh` vs `paradi-gateway`).
4
+
5
+ ## Naming (5.0.0)
6
+
7
+ Agent-facing tools use ACCESS/Context naming: `access_check`, `shell_exec`, `context_derive`, `contextKey`, `target=landing|pod|context`. Error code: `ACCESS_UNAVAILABLE`.
8
+
9
+ | ACCESS_KIND | Landing Context | `shell_exec` / `target=landing` means |
10
+ |-------------|-----------------|----------------------------------------|
11
+ | `ssh` (default) | ACCESS host | ACCESS OS |
12
+ | `paradi-gateway` | TARGET host | TARGET OS (not the gateway) |
13
+
14
+ User config is `ACCESS_*` / `TARGET_*` only. Async copy root: `ACCESS_COPY_JOB_ROOT`.
15
+
16
+ ## Architecture (read this first)
17
+
18
+ **There is no direct connection from the user's laptop to pods or cluster nodes.**
19
+
20
+ ### Shape A — ACCESS_KIND=ssh
21
+
22
+ ```
23
+ ┌─────────────┐ Hop 1 (automatic) ┌──────────┐ Hop 2 (tools) ┌────────────┐
24
+ │ MCP Client │ ─── SSH (ssh2) ──────► │ ACCESS │ ── kubectl exec ────► │ Pod │
25
+ │ + MCP Server│ │ landing │ ── ssh/sshpass ─────► │ Host/node │
26
+ └─────────────┘ │ │ ── shell / docker ──► │ ACCESS OS │
27
+ └──────────┘ └────────────┘
28
+ ```
29
+
30
+ `TARGET_*` (optional) = **default derive selector only** — not a second Context at init. `ssh_exec` = derive + shell.
31
+
32
+ ### Shape B — ACCESS_KIND=paradi-gateway
33
+
34
+ ```
35
+ ┌─────────────┐ Hop 1 ┌──────────┐
36
+ │ MCP Client │ ── SSH (encoded user) ►│ Gateway │──► landing Context = TARGET
37
+ └─────────────┘ ACCESS_USER/TARGET_HOST/TARGET_USER
38
+ ```
39
+
40
+ Runtime tools run on **TARGET**. Disabled: `context_derive`, `remote_*` / `remote_copy_*` with `target=context`. Use `shell_exec` / `target=landing` / `docker_*` (omit contextKey). SFTP is `degraded` — avoid parallel hammering; prefer smaller copies / `remote_copy_start`.
41
+
42
+ | Hop | Who initiates | What |
43
+ |-----|---------------|------|
44
+ | **Hop 1** | MCP server on user's machine | SSH to `ACCESS_HOST` (always; not exposed as a tool) |
45
+ | **Hop 2** | Landing Context | `shell_exec`, `k8s_*`, `ssh_exec` (ssh only), `docker_*`, `remote_*` |
46
+
47
+ **Common misconception:** `ssh_exec` is **NOT** "local laptop SSH to remote host". It means **ACCESS SSHs to the target** (`ACCESS_KIND=ssh` only).
48
+
49
+ ## Capability boundary
50
+
51
+ Remote execution primitives only. No project-specific paths or namespace defaults.
52
+
53
+ - `docker_*` auto-probes `capabilities.docker` (unknown ≠ true).
54
+ - `k8s_*` does **not** gate on `capabilities.kubectl` — failures surface as exitCode; use `context_probe` for presence.
55
+
56
+ ## Three Runtime paths (ssh)
57
+
58
+ | Path | Tool(s) | Action |
59
+ |------|---------|--------|
60
+ | **Landing shell** | `shell_exec`, `remote_*` target=landing | Run on ACCESS (or TARGET under Paradi) |
61
+ | **Pod** | `k8s_exec`, `k8s_logs`, `remote_*` target=pod | kubectl exec / logs |
62
+ | **Derived host** | `context_derive` → `ssh_exec`, `remote_*` target=context | ssh/sshpass — **ssh kind only** |
63
+
64
+ ### Decision matrix
65
+
66
+ | User intent | Tool (ssh) | Tool (paradi) |
67
+ |-------------|------------|---------------|
68
+ | List/find pods | `k8s_list_pods` | same |
69
+ | Run script in container | `k8s_exec` | same |
70
+ | Read file in container | `remote_read_file` target=pod | same |
71
+ | Host OS shell on landing | `shell_exec` | `shell_exec` (TARGET) |
72
+ | Host OS after jump | `context_derive` + `ssh_exec` / target=context | **disabled** — already on TARGET |
73
+ | Docker on landing | `docker_*` | `docker_*` (no host) |
74
+ | Docker on worker | `docker_*` + contextKey | **disabled** under Paradi |
75
+ | Copy short | `remote_copy_file` | same; no `target=context` under Paradi |
76
+ | Copy large | `remote_copy_start` | same; SFTP degraded |
77
+
78
+ ## ssh_exec vs k8s_exec vs docker_* vs shell_exec
79
+
80
+ | Tool | Reaches | Auth / notes |
81
+ |------|---------|----------------|
82
+ | `shell_exec` | Landing OS shell | ACCESS_* (Paradi: TARGET via gateway); optional contextKey |
83
+ | `k8s_exec` | Inside pod container | Landing kubeconfig |
84
+ | `ssh_exec` | Host OS (derive + shell) | ACCESS→host / TARGET_*; unavailable under Paradi |
85
+ | `docker_ps` / `docker_exec` / `docker_logs` | Docker on landing or derived Context | SessionPool; contextKey; auto-probes docker; unknown ≠ true |
86
+ | `context_probe` | Same Context | Returns docker/kubectl tri-state |
87
+
88
+ **Derived-host defaults (`TARGET_USER` / `TARGET_PASSWORD`):** optional under ssh. When both set, omit `user`/`password`. Password-only: omit password, still pass user. See one-line status in server instructions. Under Paradi, derive guidance is replaced by a “landing=TARGET / derive unsupported” note.
89
+
90
+ **Docker vs k8s:** container on host Docker engine → `docker_*`; container in Kubernetes → `k8s_exec` / `k8s_logs`.
91
+
92
+ ## Handshake behavior
93
+
94
+ 1. Connectivity unknown / first remote ops: `access_check` (ACCESS SSH probe ≤10s; ephemeral; not kubectl)
95
+ 2. Kubernetes ops only: then `shell_exec({"command":"kubectl cluster-info --request-timeout=5s"})`
96
+
97
+ If `access_check.reachable=false`: stop; read JSON `message`/`hints` (VPN / `ACCESS_HOST`). If other tools return `code=ACCESS_UNAVAILABLE`: stop; Chinese hints are usually in `stderr`.
98
+
99
+ Always obtain from user or context: namespace, container name for multi-container pods. For host SSH (ssh kind): only ask user/password when `TARGET_*` are unset.
100
+
101
+ ## Tool return format
102
+
103
+ On **success** (`exitCode===0`), remote command output (stdout and stderr combined) is in `stdout`; the `stderr` field is empty.
104
+
105
+ On **command failure** (`exitCode≠0`, `success=false`, no thrown exception): merged remote output is still in **`stdout`**. Only tool/connection exceptions (e.g. `ACCESS_UNAVAILABLE`) put the message in `stderr`.
106
+
107
+ Always check both `success` / `exitCode` and `stdout`.
108
+
109
+ ## Shell / I/O pitfalls
110
+
111
+ | Topic | Guidance |
112
+ |-------|----------|
113
+ | Shell metacharacters | Every remote command is parsed by bash. Quote `&`, `\|`, `>`, `$`, and URLs. Prefer `remote_copy_file` of an LF script, then `bash script.sh`. |
114
+ | Non-TTY docker | `docker login` / `pull` often exit 0 with empty stdout — use `exitCode` and follow-up probe. |
115
+ | CRLF scripts | Ship LF, or `sed -i 's/\r$//' file` / `tr -d '\r'`. |
116
+ | DOCKER_CONFIG | If permission denied: `export DOCKER_CONFIG=$HOME/dockertmp && mkdir -p "$DOCKER_CONFIG"`. |
117
+ | Registry SOP | Host with egress; `docker login` once; quoted Registry API probe. Never bare `&` in URLs. |
118
+
119
+ ## MCP prompts
120
+
121
+ | Prompt | Purpose |
122
+ |--------|---------|
123
+ | `tool-selection` | Pick the right tool for a scenario |
124
+ | `remote-debug-workflow` | list → read/tail → ps workflow |
125
+
126
+ ## Error handling
127
+
128
+ | Situation | Action |
129
+ |-----------|--------|
130
+ | `truncatedByLines: true` | Increase `maxLines`, use `offset`, or use `remote_copy_file` |
131
+ | `truncatedByChars: true` | Raise `MAX_OUTPUT_CHARS` or use `remote_copy_file` |
132
+ | `ssh_exec` under Paradi | Expected error — use `shell_exec` / `target=landing` |
133
+ | `ssh_exec` Permission denied | Add password or use `k8s_exec` for pods |
134
+ | Command `success=false` / exit≠0 | Read **stdout** |
135
+ | Empty stdout + exit 0 (docker) | Normal for non-TTY |
136
+ | `k8s_logs` empty | Use `remote_read_file` mode=tail for file logs |
137
+ | Large copy times out | Raise `totalTimeoutMs`; Paradi: expect SFTP flakiness |
138
+
139
+ ## Large file copy (local ↔ landing)
140
+
141
+ For multi-GB landing↔local (or pod→local last hop):
142
+
143
+ ```json
144
+ {
145
+ "source": { "target": "landing", "path": "/tmp/big.pt" },
146
+ "dest": { "target": "local", "path": "C:/data/big.pt" },
147
+ "skipIfComplete": true,
148
+ "totalTimeoutMs": 3600000,
149
+ "verify": "size"
150
+ }
151
+ ```
152
+
153
+ - Progress: throttled (`progressIntervalMs`, default 5s); MCP `notifications/progress` if client sends `progressToken`
154
+ - `skipIfComplete`: skip gate follows `verify`
155
+ - Result includes `transferId`; async jobs expose `remote_copy_status`
156
+
157
+ | Layer | Responsibility |
158
+ |-------|----------------|
159
+ | **host-execution-mcp** | Access-mediated remote exec |
160
+ | **User Skill** | Namespace, paths, scripts |
161
+ | **User** | Approving destructive ops |
@@ -0,0 +1,130 @@
1
+ # Configuration
2
+
3
+ ## Access shapes (user mental model)
4
+
5
+ | Shape | Env | Meaning |
6
+ |-------|-----|---------|
7
+ | Single host | `ACCESS_*` only | Land on ACCESS; shell / kubectl / docker there |
8
+ | Bastion → target | `ACCESS_*` + `TARGET_*` | Land on ACCESS; `TARGET_*` is **default derive selector** (not a second Context at init) |
9
+ | Paradi gateway | `ACCESS_KIND=paradi-gateway` + `TARGET_HOST` + `TARGET_USER` | SSH to gateway with encoded identity; landing Context = TARGET |
10
+
11
+ Runtime (shell / docker / kubectl) is **not** configured in mcp.json — Agent chooses from Context capabilities.
12
+
13
+ ## Required environment variables
14
+
15
+ | Variable | Description |
16
+ |----------|-------------|
17
+ | `ACCESS_HOST` | First SSH endpoint (jump host, direct target, or Paradi gateway) |
18
+ | `ACCESS_USER` | SSH username for ACCESS |
19
+
20
+ ## Optional environment variables
21
+
22
+ | Variable | Default | Description |
23
+ |----------|---------|-------------|
24
+ | `ACCESS_KIND` | `ssh` | `ssh` \| `paradi-gateway` |
25
+ | `ACCESS_PORT` | `22` | SSH port |
26
+ | `ACCESS_PRIVATE_KEY_PATH` | — | Explicit private key path |
27
+ | `ACCESS_PASSWORD` | — | Password auth (dev only); also used to build Paradi compound password when that mode wins |
28
+ | `ACCESS_PARADI_AUTH` | `auto` | Paradi only: `auto` \| `slash` \| `compound`. Default `auto` tries slash then compound on AUTH_FAILED and **pins** the winner for this MCP process |
29
+ | `TARGET_HOST` | — | Default derive host (`ssh`) or Paradi landing host |
30
+ | `TARGET_USER` | — | **Required if `TARGET_HOST` set** (no fallback to `ACCESS_USER`) |
31
+ | `TARGET_PASSWORD` | — | Default derive password (sshpass); ignored for Paradi auth |
32
+ | `DEFAULT_TIMEOUT_MS` | `60000` | Default command timeout |
33
+ | `MAX_OUTPUT_CHARS` | `65536` | Max tool output size |
34
+
35
+ ## Policy (not Access)
36
+
37
+ | Variable | Description |
38
+ |----------|-------------|
39
+ | `ACCESS_POLICY_ALLOWED_NAMESPACES` | Comma-separated kubectl namespace whitelist |
40
+ | `ALLOWED_NAMESPACES` | Same (kept as short alias for policy only) |
41
+
42
+ ## Ops (not Access matrix)
43
+
44
+ | Variable | Description |
45
+ |----------|-------------|
46
+ | `ACCESS_COPY_JOB_ROOT` | Async copy job state root (default `~/.agent-compose/remote-copy`) |
47
+
48
+ ## Removed in 4.0.0
49
+
50
+ Do **not** set: `BASTION_HOST`, `BASTION_USER`, `BASTION_PORT`, `BASTION_PASSWORD`, `BASTION_PRIVATE_KEY_PATH`, `REMOTE_SSH_USER`, `REMOTE_SSH_PASSWORD`.
51
+ If present, `loadConfig` throws with rename hints (`BASTION_HOST` → `ACCESS_HOST`, etc.).
52
+
53
+ ## SSH authentication (ACCESS)
54
+
55
+ Configure **one or more** methods; the server passes all of them to the SSH client, which tries each until one succeeds:
56
+
57
+ | Method | When used |
58
+ |--------|-----------|
59
+ | `ACCESS_PRIVATE_KEY_PATH` | File exists at the given path |
60
+ | Default `~/.ssh/id_ed25519`, `id_rsa`, `id_ecdsa` | Used when no explicit key path is set and a default key file exists |
61
+ | SSH agent (`SSH_AUTH_SOCK`) | Socket path is set and the socket file exists |
62
+ | `ACCESS_PASSWORD` | Variable is set |
63
+
64
+ **Examples:**
65
+
66
+ - Key or agent only (passwordless): configure keys/agent; do not set `ACCESS_PASSWORD`.
67
+ - Password only: set `ACCESS_PASSWORD`; no keys required.
68
+ - Agent socket present but empty (common in IDE-launched MCP): also set `ACCESS_PASSWORD`.
69
+
70
+ ## MCP client setup
71
+
72
+ The server uses **stdio transport**. See [clients/](clients/).
73
+
74
+ ### Minimal
75
+
76
+ ```json
77
+ {
78
+ "mcpServers": {
79
+ "host-execution": {
80
+ "command": "npx",
81
+ "args": ["-y", "host-execution-mcp@latest"],
82
+ "env": {
83
+ "ACCESS_HOST": "10.0.0.1",
84
+ "ACCESS_USER": "admin"
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### With default derive target
92
+
93
+ ```json
94
+ {
95
+ "ACCESS_KIND": "ssh",
96
+ "ACCESS_HOST": "10.121.120.7",
97
+ "ACCESS_USER": "mccxadmin",
98
+ "ACCESS_PASSWORD": "<secret>",
99
+ "TARGET_HOST": "10.121.35.68",
100
+ "TARGET_USER": "mccuseradmin",
101
+ "TARGET_PASSWORD": "<secret>"
102
+ }
103
+ ```
104
+
105
+ ### With Paradi gateway
106
+
107
+ ```json
108
+ {
109
+ "ACCESS_KIND": "paradi-gateway",
110
+ "ACCESS_HOST": "10.121.61.3",
111
+ "ACCESS_USER": "bastion-login",
112
+ "ACCESS_PASSWORD": "<secret>",
113
+ "TARGET_HOST": "10.121.35.68",
114
+ "TARGET_USER": "mccuseradmin"
115
+ }
116
+ ```
117
+
118
+ Customers only set gateway + target. Do **not** put a three-part SSH username or `@##@` password in mcp.json — the server encodes them.
119
+
120
+ Default `ACCESS_PARADI_AUTH=auto` (may omit):
121
+
122
+ 1. Try **slash** username `ACCESS_USER/TARGET_HOST/TARGET_USER` + `ACCESS_PASSWORD`
123
+ 2. On AUTH_FAILED only, try **compound** username `ACCESS_USER` + `ACCESS_PASSWORD@##@TARGET_HOST:SSH:22:TARGET_USER`
124
+ 3. Pin the winner for this MCP process (restart MCP to clear / re-probe)
125
+
126
+ Ops override: set `ACCESS_PARADI_AUTH=slash` or `compound` to force one encoding. `access_check` may report `paradiAuth` after success.
127
+
128
+ Landing Context is already TARGET: use `shell_exec` / `docker_*` (omit contextKey).
129
+
130
+ See [PARADI_ACCESS_PLAN.md](PARADI_ACCESS_PLAN.md) for the full Access / Runtime model.
@@ -0,0 +1,9 @@
1
+ # Copy specialized test checklist
2
+
3
+ Hop1 3.0.0 全面端到端清单已迁移至:
4
+
5
+ - **[`docs/HOP1_E2E_CHECKLIST.md`](HOP1_E2E_CHECKLIST.md)** — A1–J4(43 项,最近 **43/43 pass**)
6
+ - **[`docs/HOP1_E2E_RESULTS.json`](HOP1_E2E_RESULTS.json)** — 原始结果
7
+ - Runner:`scripts/hop1-e2e-one.mjs` / `scripts/hop1-e2e-all.mjs`
8
+
9
+ 旧 T1–T8(`scripts/specialized-one.mjs`)仍可用,映射见 HOP1_E2E_CHECKLIST 文末。