@hackerai/local 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -20
- package/dist/__tests__/utils.test.d.ts +1 -4
- package/dist/__tests__/utils.test.d.ts.map +1 -1
- package/dist/__tests__/utils.test.js +1 -93
- package/dist/__tests__/utils.test.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -197
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +0 -30
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -52
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
- package/dist/pty-manager.d.ts +0 -95
- package/dist/pty-manager.d.ts.map +0 -1
- package/dist/pty-manager.js +0 -452
- package/dist/pty-manager.js.map +0 -1
package/dist/utils.d.ts
CHANGED
|
@@ -4,41 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const MAX_OUTPUT_SIZE = 12288;
|
|
6
6
|
export declare const TRUNCATION_MARKER = "\n\n[... OUTPUT TRUNCATED - middle content removed to fit context limits ...]\n\n";
|
|
7
|
-
/**
|
|
8
|
-
* Required Docker capabilities for penetration testing tools.
|
|
9
|
-
* - NET_RAW: ping, nmap, masscan, hping3, arp-scan, tcpdump, raw sockets
|
|
10
|
-
* - NET_ADMIN: network interface manipulation, arp-scan, netdiscover
|
|
11
|
-
* - SYS_PTRACE: gdb, strace, ltrace (debugging tools)
|
|
12
|
-
*/
|
|
13
|
-
export declare const DOCKER_CAPABILITIES: readonly ["NET_RAW", "NET_ADMIN", "SYS_PTRACE"];
|
|
14
7
|
/**
|
|
15
8
|
* Truncates output using 25% head + 75% tail strategy.
|
|
16
9
|
* This preserves both the command start (context) and the end (final results/errors).
|
|
17
10
|
*/
|
|
18
11
|
export declare function truncateOutput(content: string, maxSize?: number): string;
|
|
19
|
-
/**
|
|
20
|
-
* Build Docker capability flags for the docker run command.
|
|
21
|
-
*/
|
|
22
|
-
export declare function buildDockerCapabilityFlags(): string;
|
|
23
|
-
/**
|
|
24
|
-
* Build the full docker run command for creating a container.
|
|
25
|
-
*/
|
|
26
|
-
export declare function buildDockerRunCommand(options: {
|
|
27
|
-
image: string;
|
|
28
|
-
containerName?: string;
|
|
29
|
-
capabilities?: boolean;
|
|
30
|
-
}): string;
|
|
31
|
-
/**
|
|
32
|
-
* Determine the sandbox mode based on configuration.
|
|
33
|
-
*/
|
|
34
|
-
export declare function getSandboxMode(config: {
|
|
35
|
-
dangerous?: boolean;
|
|
36
|
-
}): "docker" | "dangerous";
|
|
37
|
-
/**
|
|
38
|
-
* Parse shell detection output to find available shell.
|
|
39
|
-
* Returns the first valid shell path found.
|
|
40
|
-
*/
|
|
41
|
-
export declare function parseShellDetectionOutput(output: string): string;
|
|
42
12
|
export interface ShellConfig {
|
|
43
13
|
shell: string;
|
|
44
14
|
shellFlag: string;
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAGrC,eAAO,MAAM,iBAAiB,sFACuD,CAAC;AAEtF
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAGrC,eAAO,MAAM,iBAAiB,sFACuD,CAAC;AAEtF;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,MAAwB,GAChC,MAAM,CAcR;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAM7D"}
|
package/dist/utils.js
CHANGED
|
@@ -4,28 +4,13 @@
|
|
|
4
4
|
* Extracted for testability.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.
|
|
7
|
+
exports.TRUNCATION_MARKER = exports.MAX_OUTPUT_SIZE = void 0;
|
|
8
8
|
exports.truncateOutput = truncateOutput;
|
|
9
|
-
exports.buildDockerCapabilityFlags = buildDockerCapabilityFlags;
|
|
10
|
-
exports.buildDockerRunCommand = buildDockerRunCommand;
|
|
11
|
-
exports.getSandboxMode = getSandboxMode;
|
|
12
|
-
exports.parseShellDetectionOutput = parseShellDetectionOutput;
|
|
13
9
|
exports.getDefaultShell = getDefaultShell;
|
|
14
10
|
// Align with LLM context limits (~4096 tokens ≈ 12288 chars)
|
|
15
11
|
exports.MAX_OUTPUT_SIZE = 12288;
|
|
16
12
|
// Truncation marker for 25% head + 75% tail strategy
|
|
17
13
|
exports.TRUNCATION_MARKER = "\n\n[... OUTPUT TRUNCATED - middle content removed to fit context limits ...]\n\n";
|
|
18
|
-
/**
|
|
19
|
-
* Required Docker capabilities for penetration testing tools.
|
|
20
|
-
* - NET_RAW: ping, nmap, masscan, hping3, arp-scan, tcpdump, raw sockets
|
|
21
|
-
* - NET_ADMIN: network interface manipulation, arp-scan, netdiscover
|
|
22
|
-
* - SYS_PTRACE: gdb, strace, ltrace (debugging tools)
|
|
23
|
-
*/
|
|
24
|
-
exports.DOCKER_CAPABILITIES = [
|
|
25
|
-
"NET_RAW",
|
|
26
|
-
"NET_ADMIN",
|
|
27
|
-
"SYS_PTRACE",
|
|
28
|
-
];
|
|
29
14
|
/**
|
|
30
15
|
* Truncates output using 25% head + 75% tail strategy.
|
|
31
16
|
* This preserves both the command start (context) and the end (final results/errors).
|
|
@@ -42,42 +27,6 @@ function truncateOutput(content, maxSize = exports.MAX_OUTPUT_SIZE) {
|
|
|
42
27
|
const tail = content.slice(-tailBudget);
|
|
43
28
|
return head + exports.TRUNCATION_MARKER + tail;
|
|
44
29
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Build Docker capability flags for the docker run command.
|
|
47
|
-
*/
|
|
48
|
-
function buildDockerCapabilityFlags() {
|
|
49
|
-
return exports.DOCKER_CAPABILITIES.map((cap) => `--cap-add=${cap}`).join(" ");
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Build the full docker run command for creating a container.
|
|
53
|
-
*/
|
|
54
|
-
function buildDockerRunCommand(options) {
|
|
55
|
-
const { image, containerName, capabilities = true } = options;
|
|
56
|
-
const nameFlag = containerName ? `--name ${containerName} ` : "";
|
|
57
|
-
const capFlags = capabilities ? `${buildDockerCapabilityFlags()} ` : "";
|
|
58
|
-
return `docker run -d ${nameFlag}${capFlags}--network host ${image} tail -f /dev/null`;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Determine the sandbox mode based on configuration.
|
|
62
|
-
*/
|
|
63
|
-
function getSandboxMode(config) {
|
|
64
|
-
if (config.dangerous) {
|
|
65
|
-
return "dangerous";
|
|
66
|
-
}
|
|
67
|
-
return "docker";
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Parse shell detection output to find available shell.
|
|
71
|
-
* Returns the first valid shell path found.
|
|
72
|
-
*/
|
|
73
|
-
function parseShellDetectionOutput(output) {
|
|
74
|
-
if (!output || !output.trim()) {
|
|
75
|
-
return "/bin/sh";
|
|
76
|
-
}
|
|
77
|
-
// Take first line (first result from 'command -v bash || command -v sh')
|
|
78
|
-
const shell = output.trim().split("\n")[0];
|
|
79
|
-
return shell || "/bin/sh";
|
|
80
|
-
}
|
|
81
30
|
/**
|
|
82
31
|
* Get the default shell for a given platform.
|
|
83
32
|
* On Windows, uses cmd.exe (not PowerShell, which aliases curl to Invoke-WebRequest
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAaH,wCAiBC;AAYD,0CAMC;AA9CD,6DAA6D;AAChD,QAAA,eAAe,GAAG,KAAK,CAAC;AAErC,qDAAqD;AACxC,QAAA,iBAAiB,GAC5B,mFAAmF,CAAC;AAEtF;;;GAGG;AACH,SAAgB,cAAc,CAC5B,OAAe,EACf,UAAkB,uBAAe;IAEjC,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAE9C,MAAM,YAAY,GAAG,yBAAiB,CAAC,MAAM,CAAC;IAC9C,MAAM,gBAAgB,GAAG,OAAO,GAAG,YAAY,CAAC;IAEhD,+BAA+B;IAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACvD,MAAM,UAAU,GAAG,gBAAgB,GAAG,UAAU,CAAC;IAEjD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC;IAExC,OAAO,IAAI,GAAG,yBAAiB,GAAG,IAAI,CAAC;AACzC,CAAC;AAOD;;;;GAIG;AACH,SAAgB,eAAe,CAAC,QAAgB;IAC9C,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC/C,CAAC;IACD,yCAAyC;IACzC,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AACjD,CAAC"}
|
package/package.json
CHANGED
package/dist/pty-manager.d.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PTY Session Manager for the local sandbox client.
|
|
3
|
-
*
|
|
4
|
-
* Manages node-pty processes, batches output for Convex relay,
|
|
5
|
-
* and handles stdin/resize/kill from the backend.
|
|
6
|
-
*
|
|
7
|
-
* Cross-platform: macOS (forkpty), Linux (forkpty), Windows (conpty).
|
|
8
|
-
*/
|
|
9
|
-
import type { ConvexClient } from "convex/browser";
|
|
10
|
-
interface SignedSession {
|
|
11
|
-
userId: string;
|
|
12
|
-
expiresAt: number;
|
|
13
|
-
signature: string;
|
|
14
|
-
}
|
|
15
|
-
export declare class PtyManager {
|
|
16
|
-
private convex;
|
|
17
|
-
private token;
|
|
18
|
-
private connectionId;
|
|
19
|
-
private session;
|
|
20
|
-
private mode;
|
|
21
|
-
private containerId?;
|
|
22
|
-
private containerShell?;
|
|
23
|
-
private sessions;
|
|
24
|
-
private sessionSubscription;
|
|
25
|
-
private ptyAvailable;
|
|
26
|
-
constructor(
|
|
27
|
-
convex: ConvexClient,
|
|
28
|
-
token: string,
|
|
29
|
-
connectionId: string,
|
|
30
|
-
session: SignedSession,
|
|
31
|
-
mode: "docker" | "dangerous",
|
|
32
|
-
containerId?: string | undefined,
|
|
33
|
-
containerShell?: string | undefined,
|
|
34
|
-
);
|
|
35
|
-
/**
|
|
36
|
-
* Fix node-pty spawn-helper permissions.
|
|
37
|
-
* npm/pnpm can strip the execute bit from prebuilt binaries during install.
|
|
38
|
-
* Without +x, posix_spawnp fails on macOS/Linux.
|
|
39
|
-
*/
|
|
40
|
-
private fixSpawnHelperPermissions;
|
|
41
|
-
/**
|
|
42
|
-
* Update the signed session (called after heartbeat refresh).
|
|
43
|
-
*/
|
|
44
|
-
updateSession(session: SignedSession): void;
|
|
45
|
-
/**
|
|
46
|
-
* Start listening for new PTY session requests from the backend.
|
|
47
|
-
*/
|
|
48
|
-
startSessionSubscription(): void;
|
|
49
|
-
/**
|
|
50
|
-
* Stop the session subscription.
|
|
51
|
-
*/
|
|
52
|
-
stopSessionSubscription(): void;
|
|
53
|
-
/**
|
|
54
|
-
* Handle a request to create a new PTY session.
|
|
55
|
-
*/
|
|
56
|
-
private handleCreateSession;
|
|
57
|
-
/**
|
|
58
|
-
* Buffer PTY output and flush periodically to reduce Convex writes.
|
|
59
|
-
*/
|
|
60
|
-
private bufferOutput;
|
|
61
|
-
/**
|
|
62
|
-
* Flush buffered output to Convex.
|
|
63
|
-
*/
|
|
64
|
-
private flushOutput;
|
|
65
|
-
/**
|
|
66
|
-
* Subscribe to stdin input from the backend for a session.
|
|
67
|
-
*/
|
|
68
|
-
private startInputSubscription;
|
|
69
|
-
/**
|
|
70
|
-
* Handle a kill request for a session (called when backend detects killed status).
|
|
71
|
-
*/
|
|
72
|
-
killSession(sessionId: string): boolean;
|
|
73
|
-
/**
|
|
74
|
-
* Resize a PTY session.
|
|
75
|
-
*/
|
|
76
|
-
resizeSession(sessionId: string, cols: number, rows: number): boolean;
|
|
77
|
-
/**
|
|
78
|
-
* Clean up a session's resources.
|
|
79
|
-
*/
|
|
80
|
-
private cleanupSession;
|
|
81
|
-
/**
|
|
82
|
-
* Clean up all sessions and stop subscriptions.
|
|
83
|
-
*/
|
|
84
|
-
cleanup(): Promise<void>;
|
|
85
|
-
/**
|
|
86
|
-
* Check if PTY support is available.
|
|
87
|
-
*/
|
|
88
|
-
get isAvailable(): boolean;
|
|
89
|
-
/**
|
|
90
|
-
* Get the number of active sessions.
|
|
91
|
-
*/
|
|
92
|
-
get activeSessionCount(): number;
|
|
93
|
-
}
|
|
94
|
-
export {};
|
|
95
|
-
//# sourceMappingURL=pty-manager.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pty-manager.d.ts","sourceRoot":"","sources":["../src/pty-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAmDnD,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAUD,qBAAa,UAAU;IAMnB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,WAAW,CAAC;IACpB,OAAO,CAAC,cAAc,CAAC;IAXzB,OAAO,CAAC,QAAQ,CAAyC;IACzD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,YAAY,CAAU;gBAGpB,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,QAAQ,GAAG,WAAW,EAC5B,WAAW,CAAC,EAAE,MAAM,YAAA,EACpB,cAAc,CAAC,EAAE,MAAM,YAAA;IAejC;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAkCjC;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAO3C;;OAEG;IACH,wBAAwB,IAAI,IAAI;IA8BhC;;OAEG;IACH,uBAAuB,IAAI,IAAI;IAO/B;;OAEG;YACW,mBAAmB;IAsIjC;;OAEG;IACH,OAAO,CAAC,YAAY;IAkBpB;;OAEG;YACW,WAAW;IAsCzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAoD9B;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAcvC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAYrE;;OAEG;IACH,OAAO,CAAC,cAAc;IActB;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B9B;;OAEG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;OAEG;IACH,IAAI,kBAAkB,IAAI,MAAM,CAE/B;CACF"}
|
package/dist/pty-manager.js
DELETED
|
@@ -1,452 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* PTY Session Manager for the local sandbox client.
|
|
4
|
-
*
|
|
5
|
-
* Manages node-pty processes, batches output for Convex relay,
|
|
6
|
-
* and handles stdin/resize/kill from the backend.
|
|
7
|
-
*
|
|
8
|
-
* Cross-platform: macOS (forkpty), Linux (forkpty), Windows (conpty).
|
|
9
|
-
*/
|
|
10
|
-
var __importDefault =
|
|
11
|
-
(this && this.__importDefault) ||
|
|
12
|
-
function (mod) {
|
|
13
|
-
return mod && mod.__esModule ? mod : { default: mod };
|
|
14
|
-
};
|
|
15
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
exports.PtyManager = void 0;
|
|
17
|
-
const os_1 = __importDefault(require("os"));
|
|
18
|
-
const fs_1 = __importDefault(require("fs"));
|
|
19
|
-
const path_1 = __importDefault(require("path"));
|
|
20
|
-
const utils_1 = require("./utils");
|
|
21
|
-
// Convex function references
|
|
22
|
-
const api = {
|
|
23
|
-
localSandbox: {
|
|
24
|
-
activatePtySession: "localSandbox:activatePtySession",
|
|
25
|
-
submitPtyOutput: "localSandbox:submitPtyOutput",
|
|
26
|
-
updatePtySessionStatus: "localSandbox:updatePtySessionStatus",
|
|
27
|
-
markPtyInputConsumed: "localSandbox:markPtyInputConsumed",
|
|
28
|
-
subscribeToPtyInput: "localSandbox:subscribeToPtyInput",
|
|
29
|
-
getPendingPtySessions: "localSandbox:getPendingPtySessions",
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
// ANSI color codes
|
|
33
|
-
const chalk = {
|
|
34
|
-
blue: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
35
|
-
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
36
|
-
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
37
|
-
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
38
|
-
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
39
|
-
gray: (s) => `\x1b[90m${s}\x1b[0m`,
|
|
40
|
-
};
|
|
41
|
-
// Output batching: buffer for ~75ms before sending to reduce Convex write frequency
|
|
42
|
-
const OUTPUT_BATCH_INTERVAL_MS = 75;
|
|
43
|
-
// Max size of a single output chunk (base64 encoded) to stay under Convex limits
|
|
44
|
-
const MAX_CHUNK_SIZE = 64 * 1024; // 64KB
|
|
45
|
-
class PtyManager {
|
|
46
|
-
convex;
|
|
47
|
-
token;
|
|
48
|
-
connectionId;
|
|
49
|
-
session;
|
|
50
|
-
mode;
|
|
51
|
-
containerId;
|
|
52
|
-
containerShell;
|
|
53
|
-
sessions = new Map();
|
|
54
|
-
sessionSubscription = null;
|
|
55
|
-
ptyAvailable;
|
|
56
|
-
constructor(
|
|
57
|
-
convex,
|
|
58
|
-
token,
|
|
59
|
-
connectionId,
|
|
60
|
-
session,
|
|
61
|
-
mode,
|
|
62
|
-
containerId,
|
|
63
|
-
containerShell,
|
|
64
|
-
) {
|
|
65
|
-
this.convex = convex;
|
|
66
|
-
this.token = token;
|
|
67
|
-
this.connectionId = connectionId;
|
|
68
|
-
this.session = session;
|
|
69
|
-
this.mode = mode;
|
|
70
|
-
this.containerId = containerId;
|
|
71
|
-
this.containerShell = containerShell;
|
|
72
|
-
// Ensure node-pty's spawn-helper binary is executable (npm may strip the +x bit)
|
|
73
|
-
this.fixSpawnHelperPermissions();
|
|
74
|
-
this.ptyAvailable = (0, utils_1.isNodePtyAvailable)();
|
|
75
|
-
if (!this.ptyAvailable) {
|
|
76
|
-
console.log(
|
|
77
|
-
chalk.yellow(
|
|
78
|
-
"⚠️ node-pty not available - PTY sessions will not be supported",
|
|
79
|
-
),
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Fix node-pty spawn-helper permissions.
|
|
85
|
-
* npm/pnpm can strip the execute bit from prebuilt binaries during install.
|
|
86
|
-
* Without +x, posix_spawnp fails on macOS/Linux.
|
|
87
|
-
*/
|
|
88
|
-
fixSpawnHelperPermissions() {
|
|
89
|
-
try {
|
|
90
|
-
// Resolve node-pty's prebuilds directory
|
|
91
|
-
const nodePtyDir = path_1.default.dirname(
|
|
92
|
-
require.resolve("node-pty/package.json"),
|
|
93
|
-
);
|
|
94
|
-
const prebuildsDir = path_1.default.join(nodePtyDir, "prebuilds");
|
|
95
|
-
if (!fs_1.default.existsSync(prebuildsDir)) return;
|
|
96
|
-
for (const platformDir of fs_1.default.readdirSync(prebuildsDir)) {
|
|
97
|
-
const helperPath = path_1.default.join(
|
|
98
|
-
prebuildsDir,
|
|
99
|
-
platformDir,
|
|
100
|
-
"spawn-helper",
|
|
101
|
-
);
|
|
102
|
-
if (!fs_1.default.existsSync(helperPath)) continue;
|
|
103
|
-
try {
|
|
104
|
-
const stat = fs_1.default.statSync(helperPath);
|
|
105
|
-
// Check if execute bit is missing for owner
|
|
106
|
-
if ((stat.mode & 0o100) === 0) {
|
|
107
|
-
fs_1.default.chmodSync(helperPath, 0o755);
|
|
108
|
-
console.debug(
|
|
109
|
-
`Fixed spawn-helper permissions: ${platformDir}/spawn-helper`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
} catch {
|
|
113
|
-
// Non-critical — may not have permission to chmod
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
// Non-critical — node-pty might not be installed
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Update the signed session (called after heartbeat refresh).
|
|
122
|
-
*/
|
|
123
|
-
updateSession(session) {
|
|
124
|
-
this.session = session;
|
|
125
|
-
// Restart subscription with new session
|
|
126
|
-
this.stopSessionSubscription();
|
|
127
|
-
this.startSessionSubscription();
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Start listening for new PTY session requests from the backend.
|
|
131
|
-
*/
|
|
132
|
-
startSessionSubscription() {
|
|
133
|
-
if (!this.ptyAvailable) return;
|
|
134
|
-
if (this.sessionSubscription) return;
|
|
135
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
-
this.sessionSubscription = this.convex.onUpdate(
|
|
137
|
-
api.localSandbox.getPendingPtySessions,
|
|
138
|
-
{
|
|
139
|
-
connectionId: this.connectionId,
|
|
140
|
-
session: {
|
|
141
|
-
userId: this.session.userId,
|
|
142
|
-
expiresAt: this.session.expiresAt,
|
|
143
|
-
signature: this.session.signature,
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
async (data) => {
|
|
147
|
-
if (data?.authError) {
|
|
148
|
-
console.debug(
|
|
149
|
-
"PTY session subscription: auth error, will refresh on next heartbeat",
|
|
150
|
-
);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (!data?.sessions) return;
|
|
154
|
-
for (const req of data.sessions) {
|
|
155
|
-
await this.handleCreateSession(req);
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Stop the session subscription.
|
|
162
|
-
*/
|
|
163
|
-
stopSessionSubscription() {
|
|
164
|
-
if (this.sessionSubscription) {
|
|
165
|
-
this.sessionSubscription();
|
|
166
|
-
this.sessionSubscription = null;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Handle a request to create a new PTY session.
|
|
171
|
-
*/
|
|
172
|
-
async handleCreateSession(req) {
|
|
173
|
-
if (this.sessions.has(req.session_id)) return; // Already created
|
|
174
|
-
console.log(chalk.cyan(`▶ PTY session: ${req.session_id.slice(0, 8)}...`));
|
|
175
|
-
try {
|
|
176
|
-
// Dynamic import of node-pty to handle cases where it's not available
|
|
177
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
178
|
-
const nodePty = require("node-pty");
|
|
179
|
-
// Validate cwd exists on the host for dangerous mode.
|
|
180
|
-
// E2B PTY sessions pass cwd="/home/user" which doesn't exist on macOS/Windows.
|
|
181
|
-
// For Docker mode, cwd is passed to `docker exec -w` so it's inside the container.
|
|
182
|
-
let effectiveCwd = req.cwd;
|
|
183
|
-
if (this.mode === "dangerous" && effectiveCwd) {
|
|
184
|
-
try {
|
|
185
|
-
if (!fs_1.default.existsSync(effectiveCwd)) {
|
|
186
|
-
console.debug(
|
|
187
|
-
`PTY cwd "${effectiveCwd}" not found, falling back to home dir`,
|
|
188
|
-
);
|
|
189
|
-
effectiveCwd = os_1.default.homedir();
|
|
190
|
-
}
|
|
191
|
-
} catch {
|
|
192
|
-
effectiveCwd = os_1.default.homedir();
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const spawnConfig = (0, utils_1.getPtySpawnConfig)({
|
|
196
|
-
platform: os_1.default.platform(),
|
|
197
|
-
mode: this.mode,
|
|
198
|
-
containerId: this.containerId,
|
|
199
|
-
containerShell: this.containerShell,
|
|
200
|
-
cols: req.cols,
|
|
201
|
-
rows: req.rows,
|
|
202
|
-
cwd: effectiveCwd,
|
|
203
|
-
env: req.env,
|
|
204
|
-
});
|
|
205
|
-
const ptyProcess = nodePty.spawn(spawnConfig.file, spawnConfig.args, {
|
|
206
|
-
name: "xterm-256color",
|
|
207
|
-
...spawnConfig.options,
|
|
208
|
-
});
|
|
209
|
-
const activeSession = {
|
|
210
|
-
sessionId: req.session_id,
|
|
211
|
-
process: ptyProcess,
|
|
212
|
-
outputBuffer: "",
|
|
213
|
-
outputSequence: 0,
|
|
214
|
-
batchTimer: null,
|
|
215
|
-
inputSubscription: null,
|
|
216
|
-
};
|
|
217
|
-
this.sessions.set(req.session_id, activeSession);
|
|
218
|
-
// Report PID and activate session
|
|
219
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
220
|
-
await this.convex.mutation(api.localSandbox.activatePtySession, {
|
|
221
|
-
token: this.token,
|
|
222
|
-
sessionId: req.session_id,
|
|
223
|
-
pid: ptyProcess.pid,
|
|
224
|
-
});
|
|
225
|
-
// Set up output handler with batching
|
|
226
|
-
ptyProcess.onData((data) => {
|
|
227
|
-
this.bufferOutput(activeSession, data);
|
|
228
|
-
});
|
|
229
|
-
// Set up exit handler
|
|
230
|
-
ptyProcess.onExit(async (e) => {
|
|
231
|
-
// Flush any remaining output
|
|
232
|
-
await this.flushOutput(activeSession);
|
|
233
|
-
// Report exit
|
|
234
|
-
try {
|
|
235
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
236
|
-
await this.convex.mutation(api.localSandbox.updatePtySessionStatus, {
|
|
237
|
-
token: this.token,
|
|
238
|
-
sessionId: req.session_id,
|
|
239
|
-
status: "exited",
|
|
240
|
-
exitCode: e.exitCode,
|
|
241
|
-
});
|
|
242
|
-
} catch (err) {
|
|
243
|
-
console.debug(`Failed to report PTY exit: ${err}`);
|
|
244
|
-
}
|
|
245
|
-
this.cleanupSession(req.session_id);
|
|
246
|
-
console.log(
|
|
247
|
-
chalk.green(
|
|
248
|
-
`✓ PTY session ${req.session_id.slice(0, 8)}... exited (code: ${e.exitCode})`,
|
|
249
|
-
),
|
|
250
|
-
);
|
|
251
|
-
});
|
|
252
|
-
// Start listening for stdin input
|
|
253
|
-
this.startInputSubscription(activeSession);
|
|
254
|
-
console.log(
|
|
255
|
-
chalk.green(
|
|
256
|
-
`✓ PTY session ${req.session_id.slice(0, 8)}... active (PID: ${ptyProcess.pid})`,
|
|
257
|
-
),
|
|
258
|
-
);
|
|
259
|
-
} catch (err) {
|
|
260
|
-
console.error(chalk.red(`✗ Failed to create PTY session: ${err}`));
|
|
261
|
-
// Report failure
|
|
262
|
-
try {
|
|
263
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
264
|
-
await this.convex.mutation(api.localSandbox.updatePtySessionStatus, {
|
|
265
|
-
token: this.token,
|
|
266
|
-
sessionId: req.session_id,
|
|
267
|
-
status: "exited",
|
|
268
|
-
exitCode: -1,
|
|
269
|
-
});
|
|
270
|
-
} catch {
|
|
271
|
-
/* ignore */
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Buffer PTY output and flush periodically to reduce Convex writes.
|
|
277
|
-
*/
|
|
278
|
-
bufferOutput(session, data) {
|
|
279
|
-
session.outputBuffer += data;
|
|
280
|
-
// If buffer is getting large, flush immediately
|
|
281
|
-
if (session.outputBuffer.length >= MAX_CHUNK_SIZE) {
|
|
282
|
-
this.flushOutput(session);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
// Otherwise, batch with a short timer
|
|
286
|
-
if (!session.batchTimer) {
|
|
287
|
-
session.batchTimer = setTimeout(() => {
|
|
288
|
-
session.batchTimer = null;
|
|
289
|
-
this.flushOutput(session);
|
|
290
|
-
}, OUTPUT_BATCH_INTERVAL_MS);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Flush buffered output to Convex.
|
|
295
|
-
*/
|
|
296
|
-
async flushOutput(session) {
|
|
297
|
-
if (session.batchTimer) {
|
|
298
|
-
clearTimeout(session.batchTimer);
|
|
299
|
-
session.batchTimer = null;
|
|
300
|
-
}
|
|
301
|
-
if (!session.outputBuffer) return;
|
|
302
|
-
const data = session.outputBuffer;
|
|
303
|
-
session.outputBuffer = "";
|
|
304
|
-
// Split into chunks if necessary
|
|
305
|
-
const chunks = [];
|
|
306
|
-
for (let i = 0; i < data.length; i += MAX_CHUNK_SIZE) {
|
|
307
|
-
const chunk = data.slice(i, i + MAX_CHUNK_SIZE);
|
|
308
|
-
// Encode as base64 to safely transport binary PTY data through Convex
|
|
309
|
-
const encoded = Buffer.from(chunk, "utf-8").toString("base64");
|
|
310
|
-
chunks.push({
|
|
311
|
-
data: encoded,
|
|
312
|
-
sequence: ++session.outputSequence,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
try {
|
|
316
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
317
|
-
await this.convex.mutation(api.localSandbox.submitPtyOutput, {
|
|
318
|
-
token: this.token,
|
|
319
|
-
sessionId: session.sessionId,
|
|
320
|
-
chunks,
|
|
321
|
-
});
|
|
322
|
-
} catch (err) {
|
|
323
|
-
console.debug(`Failed to submit PTY output: ${err}`);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Subscribe to stdin input from the backend for a session.
|
|
328
|
-
*/
|
|
329
|
-
startInputSubscription(session) {
|
|
330
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
331
|
-
session.inputSubscription = this.convex.onUpdate(
|
|
332
|
-
api.localSandbox.subscribeToPtyInput,
|
|
333
|
-
{
|
|
334
|
-
sessionId: session.sessionId,
|
|
335
|
-
session: {
|
|
336
|
-
userId: this.session.userId,
|
|
337
|
-
expiresAt: this.session.expiresAt,
|
|
338
|
-
signature: this.session.signature,
|
|
339
|
-
},
|
|
340
|
-
connectionId: this.connectionId,
|
|
341
|
-
},
|
|
342
|
-
async (data) => {
|
|
343
|
-
if (data?.authError) return;
|
|
344
|
-
if (!data?.inputs?.length) return;
|
|
345
|
-
const inputIds = [];
|
|
346
|
-
for (const input of data.inputs) {
|
|
347
|
-
// Decode base64 data and write to PTY
|
|
348
|
-
const decoded = Buffer.from(input.data, "base64").toString("utf-8");
|
|
349
|
-
try {
|
|
350
|
-
session.process.write(decoded);
|
|
351
|
-
} catch {
|
|
352
|
-
// Process may have exited
|
|
353
|
-
}
|
|
354
|
-
inputIds.push(input._id);
|
|
355
|
-
}
|
|
356
|
-
// Mark inputs as consumed
|
|
357
|
-
if (inputIds.length > 0) {
|
|
358
|
-
try {
|
|
359
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
360
|
-
await this.convex.mutation(api.localSandbox.markPtyInputConsumed, {
|
|
361
|
-
token: this.token,
|
|
362
|
-
inputIds,
|
|
363
|
-
});
|
|
364
|
-
} catch {
|
|
365
|
-
/* ignore */
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
},
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Handle a kill request for a session (called when backend detects killed status).
|
|
373
|
-
*/
|
|
374
|
-
killSession(sessionId) {
|
|
375
|
-
const session = this.sessions.get(sessionId);
|
|
376
|
-
if (!session) return false;
|
|
377
|
-
try {
|
|
378
|
-
session.process.kill();
|
|
379
|
-
} catch {
|
|
380
|
-
/* may already be dead */
|
|
381
|
-
}
|
|
382
|
-
this.cleanupSession(sessionId);
|
|
383
|
-
return true;
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Resize a PTY session.
|
|
387
|
-
*/
|
|
388
|
-
resizeSession(sessionId, cols, rows) {
|
|
389
|
-
const session = this.sessions.get(sessionId);
|
|
390
|
-
if (!session) return false;
|
|
391
|
-
try {
|
|
392
|
-
session.process.resize(cols, rows);
|
|
393
|
-
return true;
|
|
394
|
-
} catch {
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Clean up a session's resources.
|
|
400
|
-
*/
|
|
401
|
-
cleanupSession(sessionId) {
|
|
402
|
-
const session = this.sessions.get(sessionId);
|
|
403
|
-
if (!session) return;
|
|
404
|
-
if (session.batchTimer) {
|
|
405
|
-
clearTimeout(session.batchTimer);
|
|
406
|
-
}
|
|
407
|
-
if (session.inputSubscription) {
|
|
408
|
-
session.inputSubscription();
|
|
409
|
-
}
|
|
410
|
-
this.sessions.delete(sessionId);
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Clean up all sessions and stop subscriptions.
|
|
414
|
-
*/
|
|
415
|
-
async cleanup() {
|
|
416
|
-
this.stopSessionSubscription();
|
|
417
|
-
for (const [sessionId, session] of this.sessions) {
|
|
418
|
-
try {
|
|
419
|
-
session.process.kill();
|
|
420
|
-
} catch {
|
|
421
|
-
/* ignore */
|
|
422
|
-
}
|
|
423
|
-
// Report exit
|
|
424
|
-
try {
|
|
425
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
426
|
-
await this.convex.mutation(api.localSandbox.updatePtySessionStatus, {
|
|
427
|
-
token: this.token,
|
|
428
|
-
sessionId,
|
|
429
|
-
status: "exited",
|
|
430
|
-
exitCode: -1,
|
|
431
|
-
});
|
|
432
|
-
} catch {
|
|
433
|
-
/* ignore */
|
|
434
|
-
}
|
|
435
|
-
this.cleanupSession(sessionId);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Check if PTY support is available.
|
|
440
|
-
*/
|
|
441
|
-
get isAvailable() {
|
|
442
|
-
return this.ptyAvailable;
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Get the number of active sessions.
|
|
446
|
-
*/
|
|
447
|
-
get activeSessionCount() {
|
|
448
|
-
return this.sessions.size;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
exports.PtyManager = PtyManager;
|
|
452
|
-
//# sourceMappingURL=pty-manager.js.map
|