@getforgeai/cli 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +80 -0
- package/dist/bin/forgeai.d.ts +3 -0
- package/dist/bin/forgeai.js +8 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +180 -0
- package/dist/commands/auth-login.d.ts +4 -0
- package/dist/commands/auth-login.js +202 -0
- package/dist/commands/auth-logout.d.ts +2 -0
- package/dist/commands/auth-logout.js +41 -0
- package/dist/commands/auth-status.d.ts +4 -0
- package/dist/commands/auth-status.js +81 -0
- package/dist/commands/cli-list.d.ts +4 -0
- package/dist/commands/cli-list.js +40 -0
- package/dist/commands/cli-unlink.d.ts +4 -0
- package/dist/commands/cli-unlink.js +45 -0
- package/dist/commands/config-set.d.ts +5 -0
- package/dist/commands/config-set.js +63 -0
- package/dist/commands/connect.d.ts +9 -0
- package/dist/commands/connect.js +165 -0
- package/dist/commands/disconnect.d.ts +5 -0
- package/dist/commands/disconnect.js +126 -0
- package/dist/commands/journal-clear.d.ts +2 -0
- package/dist/commands/journal-clear.js +16 -0
- package/dist/commands/journal-list.d.ts +2 -0
- package/dist/commands/journal-list.js +20 -0
- package/dist/commands/journal-status.d.ts +2 -0
- package/dist/commands/journal-status.js +18 -0
- package/dist/commands/mcp-serve.d.ts +13 -0
- package/dist/commands/mcp-serve.js +74 -0
- package/dist/commands/org-list.d.ts +4 -0
- package/dist/commands/org-list.js +56 -0
- package/dist/commands/org-unlink.d.ts +4 -0
- package/dist/commands/org-unlink.js +63 -0
- package/dist/commands/reconnect.d.ts +4 -0
- package/dist/commands/reconnect.js +46 -0
- package/dist/commands/release-open.d.ts +2 -0
- package/dist/commands/release-open.js +43 -0
- package/dist/commands/session-respond.d.ts +2 -0
- package/dist/commands/session-respond.js +29 -0
- package/dist/commands/task-open.d.ts +2 -0
- package/dist/commands/task-open.js +42 -0
- package/dist/commands/vm.d.ts +21 -0
- package/dist/commands/vm.js +122 -0
- package/dist/commands/workflow-open.d.ts +2 -0
- package/dist/commands/workflow-open.js +43 -0
- package/dist/config/config-manager.d.ts +31 -0
- package/dist/config/config-manager.js +70 -0
- package/dist/config/token-manager.d.ts +11 -0
- package/dist/config/token-manager.js +87 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +22 -0
- package/dist/lib/auth-client.d.ts +1670 -0
- package/dist/lib/auth-client.js +10 -0
- package/dist/lib/branch-manager.d.ts +79 -0
- package/dist/lib/branch-manager.js +620 -0
- package/dist/lib/clone-and-checkout.d.ts +6 -0
- package/dist/lib/clone-and-checkout.js +41 -0
- package/dist/lib/connection-manager.d.ts +58 -0
- package/dist/lib/connection-manager.js +436 -0
- package/dist/lib/daemon-entry.d.ts +2 -0
- package/dist/lib/daemon-entry.js +210 -0
- package/dist/lib/daemon.d.ts +38 -0
- package/dist/lib/daemon.js +148 -0
- package/dist/lib/event-journal.d.ts +24 -0
- package/dist/lib/event-journal.js +76 -0
- package/dist/lib/ipc-client.d.ts +10 -0
- package/dist/lib/ipc-client.js +70 -0
- package/dist/lib/ipc-server.d.ts +36 -0
- package/dist/lib/ipc-server.js +70 -0
- package/dist/lib/node-version-guard.d.ts +8 -0
- package/dist/lib/node-version-guard.js +60 -0
- package/dist/lib/open-directory.d.ts +7 -0
- package/dist/lib/open-directory.js +41 -0
- package/dist/lib/repo-manager.d.ts +29 -0
- package/dist/lib/repo-manager.js +103 -0
- package/dist/lib/resilient-emitter.d.ts +53 -0
- package/dist/lib/resilient-emitter.js +160 -0
- package/dist/lib/rest-client.d.ts +164 -0
- package/dist/lib/rest-client.js +139 -0
- package/dist/lib/skill-manager.d.ts +23 -0
- package/dist/lib/skill-manager.js +167 -0
- package/dist/lib/socket-client.d.ts +68 -0
- package/dist/lib/socket-client.js +160 -0
- package/dist/mcp/mcp-probe.d.ts +11 -0
- package/dist/mcp/mcp-probe.js +106 -0
- package/dist/mcp/mcp-response.d.ts +29 -0
- package/dist/mcp/mcp-response.js +67 -0
- package/dist/mcp/mcp-server.d.ts +19 -0
- package/dist/mcp/mcp-server.js +76 -0
- package/dist/mcp/resources/project-conventions.d.ts +4 -0
- package/dist/mcp/resources/project-conventions.js +50 -0
- package/dist/mcp/resources/task-current.d.ts +4 -0
- package/dist/mcp/resources/task-current.js +51 -0
- package/dist/mcp/tools/forge-add-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-group-dependency.js +20 -0
- package/dist/mcp/tools/forge-add-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-task-dependency.js +20 -0
- package/dist/mcp/tools/forge-ask-human.d.ts +4 -0
- package/dist/mcp/tools/forge-ask-human.js +80 -0
- package/dist/mcp/tools/forge-column-done.d.ts +4 -0
- package/dist/mcp/tools/forge-column-done.js +46 -0
- package/dist/mcp/tools/forge-complete-step.d.ts +4 -0
- package/dist/mcp/tools/forge-complete-step.js +139 -0
- package/dist/mcp/tools/forge-create-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task-group.js +22 -0
- package/dist/mcp/tools/forge-create-task.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task.js +28 -0
- package/dist/mcp/tools/forge-delete-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task-group.js +17 -0
- package/dist/mcp/tools/forge-delete-task.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task.js +17 -0
- package/dist/mcp/tools/forge-get-task.d.ts +4 -0
- package/dist/mcp/tools/forge-get-task.js +17 -0
- package/dist/mcp/tools/forge-merge-back.d.ts +4 -0
- package/dist/mcp/tools/forge-merge-back.js +179 -0
- package/dist/mcp/tools/forge-release-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-release-merge.js +159 -0
- package/dist/mcp/tools/forge-release-notes.d.ts +4 -0
- package/dist/mcp/tools/forge-release-notes.js +51 -0
- package/dist/mcp/tools/forge-release-tag.d.ts +4 -0
- package/dist/mcp/tools/forge-release-tag.js +125 -0
- package/dist/mcp/tools/forge-remove-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-group-dependency.js +19 -0
- package/dist/mcp/tools/forge-remove-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-task-dependency.js +19 -0
- package/dist/mcp/tools/forge-requeue.d.ts +4 -0
- package/dist/mcp/tools/forge-requeue.js +39 -0
- package/dist/mcp/tools/forge-submit-pr.d.ts +4 -0
- package/dist/mcp/tools/forge-submit-pr.js +68 -0
- package/dist/mcp/tools/forge-update-status.d.ts +4 -0
- package/dist/mcp/tools/forge-update-status.js +77 -0
- package/dist/mcp/tools/forge-update-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task-group.js +21 -0
- package/dist/mcp/tools/forge-update-task.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task.js +25 -0
- package/dist/mcp/tools/forge-validate-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-validate-merge.js +194 -0
- package/dist/mcp/types.d.ts +39 -0
- package/dist/mcp/types.js +8 -0
- package/dist/methodology/artifact-manager.d.ts +14 -0
- package/dist/methodology/artifact-manager.js +55 -0
- package/dist/orchestrator/agent-registry.d.ts +53 -0
- package/dist/orchestrator/agent-registry.js +96 -0
- package/dist/orchestrator/agent-spawner.d.ts +112 -0
- package/dist/orchestrator/agent-spawner.js +1518 -0
- package/dist/orchestrator/connectors/claude-code-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/claude-code-connector.js +60 -0
- package/dist/orchestrator/connectors/connector.d.ts +95 -0
- package/dist/orchestrator/connectors/connector.js +25 -0
- package/dist/orchestrator/connectors/docker-connector.d.ts +14 -0
- package/dist/orchestrator/connectors/docker-connector.js +337 -0
- package/dist/orchestrator/connectors/native-vm-connector.d.ts +11 -0
- package/dist/orchestrator/connectors/native-vm-connector.js +162 -0
- package/dist/orchestrator/connectors/opencode-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/opencode-connector.js +39 -0
- package/dist/orchestrator/connectors/vm-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/vm-connector.js +139 -0
- package/dist/orchestrator/dispatch-queue.d.ts +43 -0
- package/dist/orchestrator/dispatch-queue.js +93 -0
- package/dist/orchestrator/resume-handler.d.ts +34 -0
- package/dist/orchestrator/resume-handler.js +159 -0
- package/dist/orchestrator/stream-batcher.d.ts +16 -0
- package/dist/orchestrator/stream-batcher.js +50 -0
- package/dist/orchestrator/stream-json-extractor.d.ts +97 -0
- package/dist/orchestrator/stream-json-extractor.js +579 -0
- package/dist/vm/claude-token-manager.d.ts +36 -0
- package/dist/vm/claude-token-manager.js +223 -0
- package/dist/vm/docker-executor.d.ts +131 -0
- package/dist/vm/docker-executor.js +360 -0
- package/dist/vm/docker-process-proxy.d.ts +31 -0
- package/dist/vm/docker-process-proxy.js +105 -0
- package/dist/vm/forge-vm-agent.d.ts +11 -0
- package/dist/vm/forge-vm-agent.js +159 -0
- package/dist/vm/git-credentials.d.ts +43 -0
- package/dist/vm/git-credentials.js +53 -0
- package/dist/vm/image-manager.d.ts +7 -0
- package/dist/vm/image-manager.js +61 -0
- package/dist/vm/log.d.ts +9 -0
- package/dist/vm/log.js +18 -0
- package/dist/vm/mcp-bridge.d.ts +63 -0
- package/dist/vm/mcp-bridge.js +243 -0
- package/dist/vm/mcp-stub.d.ts +15 -0
- package/dist/vm/mcp-stub.js +67 -0
- package/dist/vm/native-addon.d.ts +69 -0
- package/dist/vm/native-addon.js +68 -0
- package/dist/vm/provider-resolver.d.ts +7 -0
- package/dist/vm/provider-resolver.js +27 -0
- package/dist/vm/providers/lima.d.ts +17 -0
- package/dist/vm/providers/lima.js +191 -0
- package/dist/vm/providers/qemu.d.ts +28 -0
- package/dist/vm/providers/qemu.js +260 -0
- package/dist/vm/providers/vfkit.d.ts +40 -0
- package/dist/vm/providers/vfkit.js +371 -0
- package/dist/vm/providers/wsl2.d.ts +27 -0
- package/dist/vm/providers/wsl2.js +226 -0
- package/dist/vm/session-manager.d.ts +130 -0
- package/dist/vm/session-manager.js +494 -0
- package/dist/vm/session-snapshot.d.ts +31 -0
- package/dist/vm/session-snapshot.js +180 -0
- package/dist/vm/types.d.ts +73 -0
- package/dist/vm/types.js +2 -0
- package/dist/vm/vm-helper-client.d.ts +81 -0
- package/dist/vm/vm-helper-client.js +335 -0
- package/dist/vm/vm-helper.d.ts +11 -0
- package/dist/vm/vm-helper.js +156 -0
- package/dist/vm/vm-ipc-protocol.d.ts +84 -0
- package/dist/vm/vm-ipc-protocol.js +9 -0
- package/dist/vm/vm-manager-facade.d.ts +23 -0
- package/dist/vm/vm-manager-facade.js +43 -0
- package/dist/vm/vm-manager.d.ts +74 -0
- package/dist/vm/vm-manager.js +515 -0
- package/dist/vm/vm-process-proxy.d.ts +34 -0
- package/dist/vm/vm-process-proxy.js +64 -0
- package/dist/vm/vsock-bridge.d.ts +44 -0
- package/dist/vm/vsock-bridge.js +140 -0
- package/dist/vm/vsock-protocol.d.ts +98 -0
- package/dist/vm/vsock-protocol.js +57 -0
- package/package.json +65 -0
- package/rootfs/Dockerfile +50 -0
- package/rootfs/forge-mcp-relay.mjs +158 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createServer } from "node:net";
|
|
5
|
+
import { spawn as spawnChild } from "node:child_process";
|
|
6
|
+
import { ensureRootfs } from "./image-manager.js";
|
|
7
|
+
import { vmLog } from "./log.js";
|
|
8
|
+
const SESSIONS_DIR = join(homedir(), ".forgeai", "vm", "sessions");
|
|
9
|
+
/** Fixed port inside VM for MCP reverse tunnel */
|
|
10
|
+
const MCP_TUNNEL_PORT = 19876;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the ForgeAI CLI binary path for spawning `forge mcp` on the host.
|
|
13
|
+
*/
|
|
14
|
+
function resolveForgeBinary() {
|
|
15
|
+
const scriptPath = process.argv[1];
|
|
16
|
+
if (scriptPath) {
|
|
17
|
+
return { command: process.execPath, prefixArgs: [scriptPath] };
|
|
18
|
+
}
|
|
19
|
+
return { command: "forgeai", prefixArgs: [] };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* TCP bridge that runs `forge mcp` on the host and exposes it as a TCP server.
|
|
23
|
+
* Each client connection (from the VM via SSH reverse tunnel) gets its own
|
|
24
|
+
* `forge mcp` subprocess with stdio piped through TCP.
|
|
25
|
+
*/
|
|
26
|
+
class McpTcpBridge {
|
|
27
|
+
context;
|
|
28
|
+
server = null;
|
|
29
|
+
port = 0;
|
|
30
|
+
constructor(context) {
|
|
31
|
+
this.context = context;
|
|
32
|
+
}
|
|
33
|
+
async start() {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
this.server = createServer((socket) => {
|
|
36
|
+
// Spawn forge mcp for each connection
|
|
37
|
+
const { command, prefixArgs } = resolveForgeBinary();
|
|
38
|
+
const mcpArgs = [
|
|
39
|
+
...prefixArgs,
|
|
40
|
+
"mcp",
|
|
41
|
+
"--task-id",
|
|
42
|
+
this.context.taskId,
|
|
43
|
+
"--project-id",
|
|
44
|
+
this.context.projectId,
|
|
45
|
+
"--org-id",
|
|
46
|
+
this.context.orgId,
|
|
47
|
+
];
|
|
48
|
+
if (this.context.workflowId) {
|
|
49
|
+
mcpArgs.push("--workflow-id", this.context.workflowId);
|
|
50
|
+
}
|
|
51
|
+
if (this.context.stepId) {
|
|
52
|
+
mcpArgs.push("--step-id", this.context.stepId);
|
|
53
|
+
}
|
|
54
|
+
const mcpProc = spawnChild(command, mcpArgs, {
|
|
55
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
56
|
+
});
|
|
57
|
+
// Bridge TCP ↔ stdio
|
|
58
|
+
socket.pipe(mcpProc.stdin);
|
|
59
|
+
mcpProc.stdout.pipe(socket);
|
|
60
|
+
socket.on("close", () => mcpProc.kill());
|
|
61
|
+
mcpProc.on("exit", () => socket.destroy());
|
|
62
|
+
});
|
|
63
|
+
this.server.listen(0, "127.0.0.1", () => {
|
|
64
|
+
const addr = this.server.address();
|
|
65
|
+
if (addr && typeof addr === "object") {
|
|
66
|
+
this.port = addr.port;
|
|
67
|
+
}
|
|
68
|
+
resolve();
|
|
69
|
+
});
|
|
70
|
+
this.server.on("error", reject);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
stop() {
|
|
74
|
+
if (this.server) {
|
|
75
|
+
this.server.close();
|
|
76
|
+
this.server = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract git credentials from the host's credential helper (e.g. osxkeychain).
|
|
82
|
+
* Returns null if no credentials found.
|
|
83
|
+
*/
|
|
84
|
+
async function extractHostGitCredentials(repoUrl) {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(repoUrl);
|
|
87
|
+
const { exec: execCb } = await import("node:child_process");
|
|
88
|
+
const { promisify } = await import("node:util");
|
|
89
|
+
const execPromise = promisify(execCb);
|
|
90
|
+
const input = `protocol=${url.protocol.replace(":", "")}\nhost=${url.hostname}\n`;
|
|
91
|
+
const { stdout } = await execPromise(`printf '${input}' | git credential fill`, { timeout: 10_000 });
|
|
92
|
+
const lines = stdout.split("\n");
|
|
93
|
+
let username = "";
|
|
94
|
+
let password = "";
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
if (line.startsWith("username="))
|
|
97
|
+
username = line.slice(9);
|
|
98
|
+
if (line.startsWith("password="))
|
|
99
|
+
password = line.slice(9);
|
|
100
|
+
}
|
|
101
|
+
if (username && password) {
|
|
102
|
+
return { username, password };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Credential helper not available or failed
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const DEFAULT_CPUS = 4;
|
|
111
|
+
const DEFAULT_MEMORY_MB = 6144;
|
|
112
|
+
const DEFAULT_VSOCK_PORT = 6789;
|
|
113
|
+
/**
|
|
114
|
+
* Network allowlist using IP CIDR ranges instead of hostnames.
|
|
115
|
+
* Iptables resolves hostnames at rule creation time only — IP ranges are reliable.
|
|
116
|
+
*
|
|
117
|
+
* Anthropic API: 104.18.0.0/16 (Cloudflare)
|
|
118
|
+
* GitHub: 140.82.112.0/20, 185.199.108.0/22, 143.55.64.0/20
|
|
119
|
+
* GitLab: 172.65.0.0/16 (Cloudflare)
|
|
120
|
+
* npm registry: 104.16.0.0/12 (Cloudflare)
|
|
121
|
+
* PyPI: 151.101.0.0/16 (Fastly)
|
|
122
|
+
* DNS: 8.8.8.8, 1.1.1.1
|
|
123
|
+
*/
|
|
124
|
+
const DEFAULT_NETWORK_ALLOWLIST = [
|
|
125
|
+
// DNS (required for git clone and package install)
|
|
126
|
+
{ cidr: "8.8.8.8/32", port: 53, protocol: "udp" },
|
|
127
|
+
{ cidr: "1.1.1.1/32", port: 53, protocol: "udp" },
|
|
128
|
+
// Anthropic API (Cloudflare)
|
|
129
|
+
{ cidr: "104.18.0.0/16", port: 443, protocol: "tcp" },
|
|
130
|
+
// GitHub
|
|
131
|
+
{ cidr: "140.82.112.0/20", port: 443, protocol: "tcp" },
|
|
132
|
+
{ cidr: "140.82.112.0/20", port: 22, protocol: "tcp" },
|
|
133
|
+
{ cidr: "185.199.108.0/22", port: 443, protocol: "tcp" },
|
|
134
|
+
{ cidr: "143.55.64.0/20", port: 443, protocol: "tcp" },
|
|
135
|
+
{ cidr: "143.55.64.0/20", port: 22, protocol: "tcp" },
|
|
136
|
+
// GitLab (Cloudflare)
|
|
137
|
+
{ cidr: "172.65.0.0/16", port: 443, protocol: "tcp" },
|
|
138
|
+
{ cidr: "172.65.0.0/16", port: 22, protocol: "tcp" },
|
|
139
|
+
// npm registry (Cloudflare)
|
|
140
|
+
{ cidr: "104.16.0.0/12", port: 443, protocol: "tcp" },
|
|
141
|
+
// PyPI (Fastly)
|
|
142
|
+
{ cidr: "151.101.0.0/16", port: 443, protocol: "tcp" },
|
|
143
|
+
];
|
|
144
|
+
let vmManagerInstance = null;
|
|
145
|
+
export function getVmManager() {
|
|
146
|
+
if (!vmManagerInstance) {
|
|
147
|
+
throw new Error("VmManager not initialized. Call initVmManager() first.");
|
|
148
|
+
}
|
|
149
|
+
return vmManagerInstance;
|
|
150
|
+
}
|
|
151
|
+
export function initVmManager(provider) {
|
|
152
|
+
vmManagerInstance = new VmManager(provider);
|
|
153
|
+
return vmManagerInstance;
|
|
154
|
+
}
|
|
155
|
+
export class VmManager {
|
|
156
|
+
provider;
|
|
157
|
+
sessions = new Map();
|
|
158
|
+
constructor(provider) {
|
|
159
|
+
this.provider = provider;
|
|
160
|
+
}
|
|
161
|
+
async createSession(taskId, config) {
|
|
162
|
+
const sanitizedId = taskId.slice(0, 12).replace(/[^a-zA-Z0-9-]/g, "");
|
|
163
|
+
const sessionId = `forgeai-${sanitizedId}`;
|
|
164
|
+
const sessionDir = join(SESSIONS_DIR, sessionId);
|
|
165
|
+
if (!existsSync(SESSIONS_DIR)) {
|
|
166
|
+
mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o700 });
|
|
167
|
+
}
|
|
168
|
+
mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
|
|
169
|
+
const session = {
|
|
170
|
+
id: sessionId,
|
|
171
|
+
taskId,
|
|
172
|
+
vmInfo: null,
|
|
173
|
+
sessionDir,
|
|
174
|
+
config,
|
|
175
|
+
status: "CREATED",
|
|
176
|
+
};
|
|
177
|
+
this.sessions.set(taskId, session);
|
|
178
|
+
vmLog.info("vm-manager", `Session created: ${sessionId} for task ${taskId}`);
|
|
179
|
+
return sessionId;
|
|
180
|
+
}
|
|
181
|
+
async bootVm(sessionId) {
|
|
182
|
+
const session = this.findSession(sessionId);
|
|
183
|
+
session.status = "BOOTING";
|
|
184
|
+
const rootfsPath = await ensureRootfs();
|
|
185
|
+
const vmInfo = await this.provider.start(sessionId, {
|
|
186
|
+
cpus: DEFAULT_CPUS,
|
|
187
|
+
memoryMB: DEFAULT_MEMORY_MB,
|
|
188
|
+
rootfsPath,
|
|
189
|
+
sessionDir: session.sessionDir,
|
|
190
|
+
networkAllowlist: DEFAULT_NETWORK_ALLOWLIST,
|
|
191
|
+
vsockPort: DEFAULT_VSOCK_PORT,
|
|
192
|
+
});
|
|
193
|
+
session.vmInfo = vmInfo;
|
|
194
|
+
session.status = "RUNNING";
|
|
195
|
+
// Mount VirtioFS shared directory (host session dir → /session in VM)
|
|
196
|
+
await this.provider.exec(sessionId, "sudo", ["mkdir", "-p", "/session"]);
|
|
197
|
+
await this.provider.exec(sessionId, "sudo", [
|
|
198
|
+
"mount",
|
|
199
|
+
"-t",
|
|
200
|
+
"virtiofs",
|
|
201
|
+
"session",
|
|
202
|
+
"/session",
|
|
203
|
+
]);
|
|
204
|
+
vmLog.dim("vm-manager", `VirtioFS mounted at /session`);
|
|
205
|
+
// Ensure swap exists (prevents OOM kills when Claude Code uses too much memory)
|
|
206
|
+
await this.provider.exec(sessionId, "sudo", [
|
|
207
|
+
"bash",
|
|
208
|
+
"-c",
|
|
209
|
+
"if [ ! -f /swapfile ]; then fallocate -l 512M /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile; fi",
|
|
210
|
+
]);
|
|
211
|
+
// Provision the VM with required tools (Node.js, Claude Code CLI)
|
|
212
|
+
await this.provisionVm(sessionId);
|
|
213
|
+
vmLog.info("vm-manager", `VM booted: ${sessionId} (CID: ${vmInfo.vsockCid ?? "N/A"})`);
|
|
214
|
+
return vmInfo;
|
|
215
|
+
}
|
|
216
|
+
async cloneRepo(sessionId, repoUrl, branch, baseBranch) {
|
|
217
|
+
if (!repoUrl) {
|
|
218
|
+
vmLog.warn("vm-manager", `No repoUrl for session ${sessionId}, skipping clone`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
vmLog.cyan("vm-manager", `Cloning repo in VM ${sessionId}...`);
|
|
222
|
+
// Ensure /workspace exists and is owned by the SSH user
|
|
223
|
+
await this.provider.exec(sessionId, "sudo", ["mkdir", "-p", "/workspace"]);
|
|
224
|
+
await this.provider.exec(sessionId, "sudo", [
|
|
225
|
+
"chown",
|
|
226
|
+
"-R",
|
|
227
|
+
"ubuntu:ubuntu",
|
|
228
|
+
"/workspace",
|
|
229
|
+
]);
|
|
230
|
+
// Resolve authenticated clone URL from host's git credential helper.
|
|
231
|
+
// Works with any credential helper: osxkeychain (macOS), wincred (Windows),
|
|
232
|
+
// credential-store/cache (Linux), GitHub CLI (gh auth setup-git).
|
|
233
|
+
// The token is injected into the URL — never persisted on the VM disk.
|
|
234
|
+
const cloneUrl = await this.resolveAuthenticatedUrl(repoUrl);
|
|
235
|
+
const cloneResult = await this.provider.exec(sessionId, "git", [
|
|
236
|
+
"clone",
|
|
237
|
+
cloneUrl,
|
|
238
|
+
"/workspace",
|
|
239
|
+
]);
|
|
240
|
+
if (cloneResult.exitCode !== 0) {
|
|
241
|
+
throw new Error(`Git clone failed in VM (exit ${cloneResult.exitCode}): ${cloneResult.stderr}`);
|
|
242
|
+
}
|
|
243
|
+
if (branch) {
|
|
244
|
+
// Try checkout — if branch doesn't exist, create it from baseBranch or HEAD
|
|
245
|
+
const checkoutResult = await this.provider.exec(sessionId, "git", [
|
|
246
|
+
"-C",
|
|
247
|
+
"/workspace",
|
|
248
|
+
"checkout",
|
|
249
|
+
branch,
|
|
250
|
+
]);
|
|
251
|
+
if (checkoutResult.exitCode !== 0) {
|
|
252
|
+
// Branch doesn't exist on remote — create from baseBranch (remote ref) or HEAD
|
|
253
|
+
const base = baseBranch ? `origin/${baseBranch}` : "HEAD";
|
|
254
|
+
vmLog.dim("vm-manager", `Branch ${branch} not found, creating from ${base}`);
|
|
255
|
+
const createResult = await this.provider.exec(sessionId, "git", [
|
|
256
|
+
"-C",
|
|
257
|
+
"/workspace",
|
|
258
|
+
"checkout",
|
|
259
|
+
"-b",
|
|
260
|
+
branch,
|
|
261
|
+
base,
|
|
262
|
+
]);
|
|
263
|
+
if (createResult.exitCode !== 0) {
|
|
264
|
+
throw new Error(`Failed to create branch ${branch} from ${base} in VM: ${createResult.stderr}`);
|
|
265
|
+
}
|
|
266
|
+
// Push the new branch to remote
|
|
267
|
+
await this.provider.exec(sessionId, "git", [
|
|
268
|
+
"-C",
|
|
269
|
+
"/workspace",
|
|
270
|
+
"push",
|
|
271
|
+
"-u",
|
|
272
|
+
"origin",
|
|
273
|
+
branch,
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
vmLog.info("vm-manager", `Checked out branch ${branch} in VM ${sessionId}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async prepareWorkspace(sessionId, files) {
|
|
280
|
+
const session = this.findSession(sessionId);
|
|
281
|
+
// Write files to host session dir (mounted via VirtioFS at /session in VM)
|
|
282
|
+
writeFileSync(join(session.sessionDir, "context.json"), files.contextJson, "utf-8");
|
|
283
|
+
writeFileSync(join(session.sessionDir, "mcp_config.json"), files.mcpConfigJson, "utf-8");
|
|
284
|
+
for (const skillFile of files.skillFiles) {
|
|
285
|
+
if (skillFile.relativePath.includes("..")) {
|
|
286
|
+
throw new Error(`Path traversal detected in skill file: ${skillFile.relativePath}`);
|
|
287
|
+
}
|
|
288
|
+
const skillPath = join(session.sessionDir, "skills", skillFile.relativePath);
|
|
289
|
+
mkdirSync(join(skillPath, ".."), { recursive: true });
|
|
290
|
+
writeFileSync(skillPath, skillFile.content, "utf-8");
|
|
291
|
+
}
|
|
292
|
+
// Copy from VirtioFS mount (/session) to workspace inside VM
|
|
293
|
+
await this.provider.exec(sessionId, "mkdir", ["-p", "/workspace/.forgeai"]);
|
|
294
|
+
await this.provider.exec(sessionId, "mkdir", ["-p", "/workspace/.claude"]);
|
|
295
|
+
await this.provider.exec(sessionId, "cp", [
|
|
296
|
+
"/session/context.json",
|
|
297
|
+
"/workspace/.forgeai/context.json",
|
|
298
|
+
]);
|
|
299
|
+
await this.provider.exec(sessionId, "cp", [
|
|
300
|
+
"/session/mcp_config.json",
|
|
301
|
+
"/workspace/.claude/mcp_config.json",
|
|
302
|
+
]);
|
|
303
|
+
if (files.skillFiles.length > 0) {
|
|
304
|
+
await this.provider.exec(sessionId, "mkdir", [
|
|
305
|
+
"-p",
|
|
306
|
+
"/workspace/.claude/skills",
|
|
307
|
+
]);
|
|
308
|
+
await this.provider.exec(sessionId, "cp", [
|
|
309
|
+
"-r",
|
|
310
|
+
"/session/skills/.",
|
|
311
|
+
"/workspace/.claude/skills/",
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
314
|
+
vmLog.info("vm-manager", `Workspace prepared in VM ${sessionId}`);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Provision the VM with required tools after boot.
|
|
318
|
+
* Skips if image is pre-baked (claude already installed via build.sh).
|
|
319
|
+
* Fallback installs Node.js + Claude CLI if using a stock Ubuntu image.
|
|
320
|
+
*/
|
|
321
|
+
async provisionVm(sessionId) {
|
|
322
|
+
// Install tools only if not pre-baked
|
|
323
|
+
const check = await this.provider.exec(sessionId, "which", ["claude"]);
|
|
324
|
+
if (check.exitCode === 0) {
|
|
325
|
+
vmLog.dim("vm-manager", `Pre-baked image — skipping tool installation`);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
vmLog.cyan("vm-manager", `Provisioning VM ${sessionId} (installing Node.js + Claude CLI)...`);
|
|
329
|
+
const installResult = await this.provider.exec(sessionId, "sudo", [
|
|
330
|
+
"bash",
|
|
331
|
+
"-c",
|
|
332
|
+
"curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs && npm install -g @anthropic-ai/claude-code",
|
|
333
|
+
]);
|
|
334
|
+
if (installResult.exitCode !== 0) {
|
|
335
|
+
throw new Error(`Failed to install Node.js and Claude CLI in VM: ${installResult.stderr}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Always inject credentials (per-session, not baked into image)
|
|
339
|
+
await this.injectClaudeCredentials(sessionId);
|
|
340
|
+
vmLog.info("vm-manager", `VM provisioned`);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Inject Claude Code credentials into the VM.
|
|
344
|
+
* Supports:
|
|
345
|
+
* - macOS Keychain (Pro/Max subscription via OAuth)
|
|
346
|
+
* - ~/.claude/.credentials.json (Linux/manual setup)
|
|
347
|
+
* - ANTHROPIC_API_KEY env var (API key fallback — handled at spawn time)
|
|
348
|
+
*/
|
|
349
|
+
async injectClaudeCredentials(sessionId) {
|
|
350
|
+
let credentials = null;
|
|
351
|
+
// 1. Try macOS Keychain (Pro/Max subscription)
|
|
352
|
+
if (process.platform === "darwin") {
|
|
353
|
+
try {
|
|
354
|
+
const { exec: execCb } = await import("node:child_process");
|
|
355
|
+
const { promisify } = await import("node:util");
|
|
356
|
+
const execPromise = promisify(execCb);
|
|
357
|
+
const { stdout } = await execPromise('security find-generic-password -s "Claude Code-credentials" -w', { timeout: 5_000 });
|
|
358
|
+
if (stdout.trim()) {
|
|
359
|
+
credentials = stdout.trim();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// No Keychain entry — fall through
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// 2. Try ~/.claude/.credentials.json (Linux/Windows or manual export)
|
|
367
|
+
if (!credentials) {
|
|
368
|
+
const credPath = join(homedir(), ".claude", ".credentials.json");
|
|
369
|
+
if (existsSync(credPath)) {
|
|
370
|
+
credentials = readFileSync(credPath, "utf-8");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (!credentials) {
|
|
374
|
+
// No credentials found — ANTHROPIC_API_KEY will be used at spawn time
|
|
375
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
376
|
+
vmLog.warn("vm-manager", `No Claude credentials found. Set ANTHROPIC_API_KEY or run 'claude /login' first.`);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// Write credentials to VM
|
|
381
|
+
vmLog.dim("vm-manager", `Injecting Claude credentials into VM`);
|
|
382
|
+
await this.provider.exec(sessionId, "mkdir", [
|
|
383
|
+
"-p",
|
|
384
|
+
"/home/ubuntu/.claude",
|
|
385
|
+
]);
|
|
386
|
+
// Use printf to write the credentials (avoids quoting issues with echo)
|
|
387
|
+
const escaped = credentials.replace(/'/g, "'\\''");
|
|
388
|
+
await this.provider.exec(sessionId, "bash", [
|
|
389
|
+
"-c",
|
|
390
|
+
`printf '%s' '${escaped}' > /home/ubuntu/.claude/.credentials.json && chmod 600 /home/ubuntu/.claude/.credentials.json`,
|
|
391
|
+
]);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Resolve an authenticated clone URL by extracting credentials from the host.
|
|
395
|
+
* Uses `git credential fill` which delegates to the user's credential helper
|
|
396
|
+
* (osxkeychain, wincred, credential-store, GitHub CLI, etc.).
|
|
397
|
+
*
|
|
398
|
+
* For SSH URLs, returns as-is (SSH agent forwarding handles auth).
|
|
399
|
+
* For HTTPS URLs, injects the token into the URL (ephemeral, never persisted in VM).
|
|
400
|
+
*/
|
|
401
|
+
async resolveAuthenticatedUrl(repoUrl) {
|
|
402
|
+
// SSH URLs don't need credential extraction
|
|
403
|
+
if (repoUrl.startsWith("git@") || repoUrl.startsWith("ssh://")) {
|
|
404
|
+
return repoUrl;
|
|
405
|
+
}
|
|
406
|
+
const creds = await extractHostGitCredentials(repoUrl);
|
|
407
|
+
if (creds) {
|
|
408
|
+
const url = new URL(repoUrl);
|
|
409
|
+
url.username = creds.username;
|
|
410
|
+
url.password = creds.password;
|
|
411
|
+
return url.toString();
|
|
412
|
+
}
|
|
413
|
+
throw new Error(`No git credentials found for ${repoUrl}. ` +
|
|
414
|
+
"Ensure your git credential helper is configured " +
|
|
415
|
+
"(macOS: osxkeychain, Windows: wincred, Linux: credential-store, or gh auth setup-git).");
|
|
416
|
+
}
|
|
417
|
+
async spawnAgent(sessionId, cmd, args, mcpContext) {
|
|
418
|
+
const session = this.findSession(sessionId);
|
|
419
|
+
if (!session.vmInfo) {
|
|
420
|
+
throw new Error(`VM not booted for session ${sessionId}`);
|
|
421
|
+
}
|
|
422
|
+
// Start MCP bridge on host if context is provided
|
|
423
|
+
let mcpBridge;
|
|
424
|
+
const reverseTunnels = [];
|
|
425
|
+
if (mcpContext) {
|
|
426
|
+
mcpBridge = new McpTcpBridge(mcpContext);
|
|
427
|
+
await mcpBridge.start();
|
|
428
|
+
reverseTunnels.push({
|
|
429
|
+
remotePort: MCP_TUNNEL_PORT,
|
|
430
|
+
localPort: mcpBridge.port,
|
|
431
|
+
});
|
|
432
|
+
vmLog.dim("vm-manager", `MCP bridge on host port ${mcpBridge.port} → VM port ${MCP_TUNNEL_PORT}`);
|
|
433
|
+
}
|
|
434
|
+
// Spawn agent via SSH streaming with reverse tunnel for MCP
|
|
435
|
+
const result = this.provider.spawnStreaming(sessionId, cmd, args, "/workspace", {
|
|
436
|
+
reverseTunnels,
|
|
437
|
+
});
|
|
438
|
+
// Clean up when agent exits: stop MCP bridge + destroy VM
|
|
439
|
+
// Capture the current VM PID to avoid destroying a replacement VM (race condition
|
|
440
|
+
// when stop-and-replace creates a new VM with the same session ID)
|
|
441
|
+
const sid = sessionId;
|
|
442
|
+
const vmPid = result.pid;
|
|
443
|
+
result.process.once("exit", () => {
|
|
444
|
+
if (mcpBridge)
|
|
445
|
+
mcpBridge.stop();
|
|
446
|
+
// Only destroy if the session still belongs to this VM (not replaced)
|
|
447
|
+
const currentSession = this.findSessionOptional(sid);
|
|
448
|
+
if (currentSession?.vmInfo?.pid === vmPid) {
|
|
449
|
+
this.destroySession(sid).catch((err) => {
|
|
450
|
+
vmLog.warn("vm-manager", `Failed to destroy session ${sid} after exit: ${err instanceof Error ? err.message : String(err)}`);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
vmLog.dim("vm-manager", `Skipping destroy for ${sid} — session replaced by new VM`);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
vmLog.info("vm-manager", `Agent spawned in VM ${sessionId} (PID: ${result.pid})`);
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
async stopVm(sessionId) {
|
|
461
|
+
try {
|
|
462
|
+
await this.provider.stop(sessionId);
|
|
463
|
+
const session = this.findSessionOptional(sessionId);
|
|
464
|
+
if (session) {
|
|
465
|
+
session.status = "STOPPED";
|
|
466
|
+
}
|
|
467
|
+
vmLog.info("vm-manager", `VM stopped: ${sessionId}`);
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
vmLog.warn("vm-manager", `Error stopping VM ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async destroySession(sessionId) {
|
|
474
|
+
try {
|
|
475
|
+
await this.provider.destroy(sessionId);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// Best-effort destroy
|
|
479
|
+
}
|
|
480
|
+
// Find task ID from session
|
|
481
|
+
for (const [taskId, session] of this.sessions) {
|
|
482
|
+
if (session.id === sessionId) {
|
|
483
|
+
this.sessions.delete(taskId);
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Clean up session directory
|
|
488
|
+
const sessionDir = join(SESSIONS_DIR, sessionId);
|
|
489
|
+
if (existsSync(sessionDir)) {
|
|
490
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
491
|
+
}
|
|
492
|
+
vmLog.info("vm-manager", `Session destroyed: ${sessionId}`);
|
|
493
|
+
}
|
|
494
|
+
getSession(taskId) {
|
|
495
|
+
return this.sessions.get(taskId);
|
|
496
|
+
}
|
|
497
|
+
getAllSessions() {
|
|
498
|
+
return Array.from(this.sessions.values());
|
|
499
|
+
}
|
|
500
|
+
findSession(sessionId) {
|
|
501
|
+
for (const session of this.sessions.values()) {
|
|
502
|
+
if (session.id === sessionId)
|
|
503
|
+
return session;
|
|
504
|
+
}
|
|
505
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
506
|
+
}
|
|
507
|
+
findSessionOptional(sessionId) {
|
|
508
|
+
for (const session of this.sessions.values()) {
|
|
509
|
+
if (session.id === sessionId)
|
|
510
|
+
return session;
|
|
511
|
+
}
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
//# sourceMappingURL=vm-manager.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventEmitter-based proxy that wraps vsock streams into an AgentProcess.
|
|
3
|
+
*
|
|
4
|
+
* The native VM addon delivers stdout/stderr data via vsock port callbacks.
|
|
5
|
+
* This class buffers that data into PassThrough streams and emits the same
|
|
6
|
+
* events (exit, close, error) that ChildProcess would, so the orchestrator's
|
|
7
|
+
* attachProcessHandlers() works identically for both local and VM agents.
|
|
8
|
+
*/
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
import { PassThrough } from "node:stream";
|
|
11
|
+
import type { AgentProcess } from "../orchestrator/connectors/connector.js";
|
|
12
|
+
export declare class VmProcessProxy extends EventEmitter implements AgentProcess {
|
|
13
|
+
private readonly sessionId;
|
|
14
|
+
private readonly _sessionIndex;
|
|
15
|
+
readonly stdout: PassThrough;
|
|
16
|
+
readonly stderr: PassThrough;
|
|
17
|
+
pid: number;
|
|
18
|
+
exitCode: number | null;
|
|
19
|
+
killed: boolean;
|
|
20
|
+
constructor(sessionId: string, _sessionIndex: number);
|
|
21
|
+
/** Called by SessionManager once vminitd reports the sandbox PID */
|
|
22
|
+
setPid(pid: number): void;
|
|
23
|
+
/** Push raw stdout data from the vsock stream */
|
|
24
|
+
pushStdout(data: Buffer): void;
|
|
25
|
+
/** Push raw stderr data from the vsock stream */
|
|
26
|
+
pushStderr(data: Buffer): void;
|
|
27
|
+
/**
|
|
28
|
+
* Kill the agent process inside the VM sandbox.
|
|
29
|
+
* Returns true if the kill signal was sent, false if the process was already dead.
|
|
30
|
+
* Per AgentProcess interface, this must be synchronous (returns boolean, not Promise).
|
|
31
|
+
*/
|
|
32
|
+
kill(signal?: string): boolean;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=vm-process-proxy.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventEmitter-based proxy that wraps vsock streams into an AgentProcess.
|
|
3
|
+
*
|
|
4
|
+
* The native VM addon delivers stdout/stderr data via vsock port callbacks.
|
|
5
|
+
* This class buffers that data into PassThrough streams and emits the same
|
|
6
|
+
* events (exit, close, error) that ChildProcess would, so the orchestrator's
|
|
7
|
+
* attachProcessHandlers() works identically for both local and VM agents.
|
|
8
|
+
*/
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
import { PassThrough } from "node:stream";
|
|
11
|
+
import { getSessionManager } from "./session-manager.js";
|
|
12
|
+
export class VmProcessProxy extends EventEmitter {
|
|
13
|
+
sessionId;
|
|
14
|
+
_sessionIndex;
|
|
15
|
+
stdout;
|
|
16
|
+
stderr;
|
|
17
|
+
pid = -1;
|
|
18
|
+
exitCode = null;
|
|
19
|
+
killed = false;
|
|
20
|
+
constructor(sessionId, _sessionIndex) {
|
|
21
|
+
super();
|
|
22
|
+
this.sessionId = sessionId;
|
|
23
|
+
this._sessionIndex = _sessionIndex;
|
|
24
|
+
this.stdout = new PassThrough();
|
|
25
|
+
this.stderr = new PassThrough();
|
|
26
|
+
// Register for exit events relayed by SessionManager's control channel handler
|
|
27
|
+
const sm = getSessionManager();
|
|
28
|
+
sm.onSessionExit(sessionId, (code) => {
|
|
29
|
+
this.exitCode = code;
|
|
30
|
+
this.stdout.end();
|
|
31
|
+
this.stderr.end();
|
|
32
|
+
this.emit("exit", code, null);
|
|
33
|
+
this.emit("close", code, null);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Called by SessionManager once vminitd reports the sandbox PID */
|
|
37
|
+
setPid(pid) {
|
|
38
|
+
this.pid = pid;
|
|
39
|
+
}
|
|
40
|
+
/** Push raw stdout data from the vsock stream */
|
|
41
|
+
pushStdout(data) {
|
|
42
|
+
this.stdout.push(data);
|
|
43
|
+
}
|
|
44
|
+
/** Push raw stderr data from the vsock stream */
|
|
45
|
+
pushStderr(data) {
|
|
46
|
+
this.stderr.push(data);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Kill the agent process inside the VM sandbox.
|
|
50
|
+
* Returns true if the kill signal was sent, false if the process was already dead.
|
|
51
|
+
* Per AgentProcess interface, this must be synchronous (returns boolean, not Promise).
|
|
52
|
+
*/
|
|
53
|
+
kill(signal) {
|
|
54
|
+
if (this.exitCode !== null || this.killed)
|
|
55
|
+
return false;
|
|
56
|
+
this.killed = true;
|
|
57
|
+
const sm = getSessionManager();
|
|
58
|
+
sm.killAgent(this.sessionId, signal ?? "SIGTERM").catch(() => {
|
|
59
|
+
/* best effort — process may already be gone */
|
|
60
|
+
});
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=vm-process-proxy.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
export declare const CHANNEL_CONTROL = 0;
|
|
3
|
+
export declare const CHANNEL_STDOUT = 1;
|
|
4
|
+
export declare const CHANNEL_STDERR = 2;
|
|
5
|
+
export declare const CHANNEL_MCP = 3;
|
|
6
|
+
export type ControlMessage = {
|
|
7
|
+
type: "spawn";
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
cwd: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: "pid";
|
|
13
|
+
pid: number;
|
|
14
|
+
} | {
|
|
15
|
+
type: "exit";
|
|
16
|
+
code: number;
|
|
17
|
+
} | {
|
|
18
|
+
type: "error";
|
|
19
|
+
message: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: "kill";
|
|
22
|
+
signal: string;
|
|
23
|
+
pid: number;
|
|
24
|
+
};
|
|
25
|
+
export declare class VsockBridge extends EventEmitter {
|
|
26
|
+
private readonly socketPath;
|
|
27
|
+
private socket;
|
|
28
|
+
private buffer;
|
|
29
|
+
private connected;
|
|
30
|
+
/**
|
|
31
|
+
* @param socketPath Unix domain socket path (vfkit exposes vsock as UDS on macOS)
|
|
32
|
+
*/
|
|
33
|
+
constructor(socketPath: string);
|
|
34
|
+
connect(): Promise<void>;
|
|
35
|
+
private setupDataHandler;
|
|
36
|
+
private processBuffer;
|
|
37
|
+
private handleFrame;
|
|
38
|
+
send(channel: number, data: Buffer | string): void;
|
|
39
|
+
sendControl(msg: ControlMessage): void;
|
|
40
|
+
sendMcp(data: Buffer | string): void;
|
|
41
|
+
disconnect(): void;
|
|
42
|
+
isConnected(): boolean;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=vsock-bridge.d.ts.map
|