@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,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level Docker CLI wrapper using child_process.
|
|
3
|
+
* Zero npm dependencies — shells out to `docker` commands directly.
|
|
4
|
+
*
|
|
5
|
+
* All containers get labels:
|
|
6
|
+
* - forgeai.managed=true
|
|
7
|
+
* - forgeai.session-id=<sessionId>
|
|
8
|
+
* - forgeai.task-id=<taskId>
|
|
9
|
+
*
|
|
10
|
+
* On Linux, adds `--add-host host.docker.internal:host-gateway` to enable
|
|
11
|
+
* communication from container to host. macOS/Windows Docker Desktop handles
|
|
12
|
+
* this automatically.
|
|
13
|
+
*/
|
|
14
|
+
import { execFile as execFileCallback, spawn as spawnCallback, } from "node:child_process";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
import { vmLog } from "./log.js";
|
|
17
|
+
const execFile = promisify(execFileCallback);
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Platform Detection
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
function getPlatformAddHost() {
|
|
22
|
+
if (process.platform === "linux") {
|
|
23
|
+
return ["--add-host", "host.docker.internal:host-gateway"];
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Network
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const FALLBACK_BRIDGE_GATEWAY_IP = "172.17.0.1";
|
|
31
|
+
let cachedBridgeGatewayIp = null;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the gateway IP of the default Docker bridge network (Linux).
|
|
34
|
+
* This is the host-side address containers reach via
|
|
35
|
+
* `--add-host host.docker.internal:host-gateway`. Cached after first lookup.
|
|
36
|
+
* Falls back to the Docker default 172.17.0.1 if inspection fails.
|
|
37
|
+
*/
|
|
38
|
+
export async function getDockerBridgeGatewayIp() {
|
|
39
|
+
if (cachedBridgeGatewayIp)
|
|
40
|
+
return cachedBridgeGatewayIp;
|
|
41
|
+
try {
|
|
42
|
+
const { stdout } = await execFile("docker", [
|
|
43
|
+
"network",
|
|
44
|
+
"inspect",
|
|
45
|
+
"bridge",
|
|
46
|
+
"--format",
|
|
47
|
+
"{{(index .IPAM.Config 0).Gateway}}",
|
|
48
|
+
], { timeout: 5000 });
|
|
49
|
+
const ip = stdout.trim();
|
|
50
|
+
if (ip.length > 0) {
|
|
51
|
+
cachedBridgeGatewayIp = ip;
|
|
52
|
+
return ip;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
vmLog.warn("docker", `Failed to inspect bridge network gateway: ${String(error)}`);
|
|
57
|
+
}
|
|
58
|
+
cachedBridgeGatewayIp = FALLBACK_BRIDGE_GATEWAY_IP;
|
|
59
|
+
return cachedBridgeGatewayIp;
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Docker Availability
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
/**
|
|
65
|
+
* Check if Docker is available on the system.
|
|
66
|
+
* Runs `docker info --format '{{.ID}}'` with 5s timeout.
|
|
67
|
+
* Returns true if exit code 0, false otherwise.
|
|
68
|
+
*/
|
|
69
|
+
export async function isDockerAvailable() {
|
|
70
|
+
try {
|
|
71
|
+
const { stdout } = await execFile("docker", ["info", "--format", "{{.ID}}"], {
|
|
72
|
+
timeout: 5000,
|
|
73
|
+
});
|
|
74
|
+
return stdout.trim().length > 0;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Image Operations
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
/**
|
|
84
|
+
* Pull a Docker image.
|
|
85
|
+
* Runs `docker pull <image>` and logs progress.
|
|
86
|
+
*/
|
|
87
|
+
export async function pullImage(image) {
|
|
88
|
+
vmLog.info("docker", `Pulling image ${image}...`);
|
|
89
|
+
try {
|
|
90
|
+
await execFile("docker", ["pull", image]);
|
|
91
|
+
vmLog.info("docker", `Successfully pulled ${image}`);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
vmLog.error("docker", `Failed to pull ${image}: ${String(error)}`);
|
|
95
|
+
throw new Error(`Docker pull failed for ${image}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check if a Docker image exists locally.
|
|
100
|
+
* Runs `docker image inspect <image>`.
|
|
101
|
+
* Returns true if exit code 0, false otherwise.
|
|
102
|
+
*/
|
|
103
|
+
export async function imageExists(image) {
|
|
104
|
+
try {
|
|
105
|
+
await execFile("docker", ["image", "inspect", image]);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Create a Docker container.
|
|
114
|
+
*
|
|
115
|
+
* Runs:
|
|
116
|
+
* docker create --name <name> --platform linux/arm64 \
|
|
117
|
+
* -v <mounts...> \
|
|
118
|
+
* -e <envs...> \
|
|
119
|
+
* --label forgeai.managed=true \
|
|
120
|
+
* --label forgeai.session-id=<sessionId> \
|
|
121
|
+
* --label forgeai.task-id=<taskId> \
|
|
122
|
+
* [--label <custom labels...>] \
|
|
123
|
+
* [--add-host host.docker.internal:host-gateway (Linux only)] \
|
|
124
|
+
* <image> sleep infinity
|
|
125
|
+
*
|
|
126
|
+
* Returns the container ID from stdout.
|
|
127
|
+
*/
|
|
128
|
+
export async function createContainer(opts) {
|
|
129
|
+
const args = [
|
|
130
|
+
"create",
|
|
131
|
+
"--name",
|
|
132
|
+
opts.name,
|
|
133
|
+
"--platform",
|
|
134
|
+
"linux/arm64",
|
|
135
|
+
];
|
|
136
|
+
// Add volume mounts
|
|
137
|
+
for (const vol of opts.volumes) {
|
|
138
|
+
const mountStr = vol.readOnly
|
|
139
|
+
? `${vol.hostPath}:${vol.containerPath}:ro`
|
|
140
|
+
: `${vol.hostPath}:${vol.containerPath}`;
|
|
141
|
+
args.push("-v", mountStr);
|
|
142
|
+
}
|
|
143
|
+
// Add environment variables
|
|
144
|
+
if (opts.env) {
|
|
145
|
+
for (const [key, value] of Object.entries(opts.env)) {
|
|
146
|
+
args.push("-e", `${key}=${value}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Add mandatory labels
|
|
150
|
+
args.push("--label", "forgeai.managed=true");
|
|
151
|
+
args.push("--label", `forgeai.session-id=${opts.sessionId}`);
|
|
152
|
+
args.push("--label", `forgeai.task-id=${opts.taskId}`);
|
|
153
|
+
// Add custom labels
|
|
154
|
+
if (opts.labels) {
|
|
155
|
+
for (const [key, value] of Object.entries(opts.labels)) {
|
|
156
|
+
args.push("--label", `${key}=${value}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Add Linux host gateway mapping
|
|
160
|
+
args.push(...getPlatformAddHost());
|
|
161
|
+
// Add image and command
|
|
162
|
+
args.push(opts.image);
|
|
163
|
+
args.push("sleep");
|
|
164
|
+
args.push("infinity");
|
|
165
|
+
try {
|
|
166
|
+
const { stdout } = await execFile("docker", args);
|
|
167
|
+
const containerId = stdout.trim();
|
|
168
|
+
vmLog.info("docker", `Created container ${containerId} (${opts.name})`);
|
|
169
|
+
return containerId;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
vmLog.error("docker", `Failed to create container: ${String(error)}`);
|
|
173
|
+
throw new Error(`Docker create failed: ${String(error)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Start a Docker container.
|
|
178
|
+
* Runs `docker start <containerId>`.
|
|
179
|
+
*/
|
|
180
|
+
export async function startContainer(containerId) {
|
|
181
|
+
try {
|
|
182
|
+
await execFile("docker", ["start", containerId]);
|
|
183
|
+
vmLog.info("docker", `Started container ${containerId}`);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
vmLog.error("docker", `Failed to start ${containerId}: ${String(error)}`);
|
|
187
|
+
throw new Error(`Docker start failed: ${String(error)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Execute a command inside a running container.
|
|
192
|
+
*
|
|
193
|
+
* Runs:
|
|
194
|
+
* docker exec [-w <cwd>] [-e KEY=VAL...] [-e SECRET...] <containerId> <cmd...>
|
|
195
|
+
*
|
|
196
|
+
* Returns { stdout, stderr, exitCode }.
|
|
197
|
+
* Does NOT throw on non-zero exit — caller can check exitCode.
|
|
198
|
+
*/
|
|
199
|
+
export async function execInContainer(containerId, cmd, opts) {
|
|
200
|
+
const args = ["exec"];
|
|
201
|
+
if (opts?.cwd) {
|
|
202
|
+
args.push("-w", opts.cwd);
|
|
203
|
+
}
|
|
204
|
+
if (opts?.env) {
|
|
205
|
+
for (const [key, value] of Object.entries(opts.env)) {
|
|
206
|
+
args.push("-e", `${key}=${value}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Name-only -e for secrets: value stays out of argv, sourced from our env.
|
|
210
|
+
if (opts?.secretEnv) {
|
|
211
|
+
for (const key of Object.keys(opts.secretEnv)) {
|
|
212
|
+
args.push("-e", key);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
args.push(containerId);
|
|
216
|
+
args.push(...cmd);
|
|
217
|
+
try {
|
|
218
|
+
const { stdout, stderr } = await execFile("docker", args, {
|
|
219
|
+
timeout: opts?.timeout,
|
|
220
|
+
env: opts?.secretEnv
|
|
221
|
+
? { ...process.env, ...opts.secretEnv }
|
|
222
|
+
: process.env,
|
|
223
|
+
});
|
|
224
|
+
return { stdout, stderr, exitCode: 0 };
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
const execError = error;
|
|
228
|
+
return {
|
|
229
|
+
stdout: execError.stdout || "",
|
|
230
|
+
stderr: execError.stderr || "",
|
|
231
|
+
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Spawn a long-running command inside a container with stdin attached.
|
|
237
|
+
*
|
|
238
|
+
* Runs:
|
|
239
|
+
* docker exec -i [-w <cwd>] [-e KEY=VAL...] <containerId> <cmd...>
|
|
240
|
+
*
|
|
241
|
+
* The `-i` flag keeps stdin open. Returns the raw ChildProcess.
|
|
242
|
+
*/
|
|
243
|
+
export function spawnInContainer(containerId, cmd, opts) {
|
|
244
|
+
const args = ["exec"];
|
|
245
|
+
// Run as non-root user (Claude Code refuses --dangerously-skip-permissions as root)
|
|
246
|
+
if (opts?.user) {
|
|
247
|
+
args.push("--user", opts.user);
|
|
248
|
+
}
|
|
249
|
+
if (opts?.cwd) {
|
|
250
|
+
args.push("-w", opts.cwd);
|
|
251
|
+
}
|
|
252
|
+
if (opts?.env) {
|
|
253
|
+
for (const [key, value] of Object.entries(opts.env)) {
|
|
254
|
+
args.push("-e", `${key}=${value}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Name-only -e for secrets: value stays out of argv, sourced from our env.
|
|
258
|
+
if (opts?.secretEnv) {
|
|
259
|
+
for (const key of Object.keys(opts.secretEnv)) {
|
|
260
|
+
args.push("-e", key);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
args.push(containerId);
|
|
264
|
+
args.push(...cmd);
|
|
265
|
+
return spawnCallback("docker", args, {
|
|
266
|
+
env: opts?.secretEnv ? { ...process.env, ...opts.secretEnv } : process.env,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Container Termination
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
/**
|
|
273
|
+
* Kill a container.
|
|
274
|
+
* Runs `docker kill [--signal <signal>] <containerId>`.
|
|
275
|
+
*/
|
|
276
|
+
export async function killContainer(containerId, signal) {
|
|
277
|
+
const args = ["kill"];
|
|
278
|
+
if (signal) {
|
|
279
|
+
args.push("--signal", signal);
|
|
280
|
+
}
|
|
281
|
+
args.push(containerId);
|
|
282
|
+
try {
|
|
283
|
+
await execFile("docker", args);
|
|
284
|
+
vmLog.info("docker", `Killed container ${containerId}`);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
vmLog.warn("docker", `Failed to kill ${containerId}: ${String(error)}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Remove a container.
|
|
292
|
+
* Runs `docker rm [-f] <containerId>`.
|
|
293
|
+
* Ignores errors (container may already be gone).
|
|
294
|
+
*/
|
|
295
|
+
export async function removeContainer(containerId, force) {
|
|
296
|
+
const args = ["rm"];
|
|
297
|
+
if (force) {
|
|
298
|
+
args.push("-f");
|
|
299
|
+
}
|
|
300
|
+
args.push(containerId);
|
|
301
|
+
try {
|
|
302
|
+
await execFile("docker", args);
|
|
303
|
+
vmLog.info("docker", `Removed container ${containerId}`);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
vmLog.dim("docker", `Container ${containerId} already removed`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* List all ForgeAI-managed containers.
|
|
311
|
+
* Runs `docker ps -a --filter label=forgeai.managed=true --format '{{json .}}'`.
|
|
312
|
+
* Parses JSON output and returns array of containers with extracted labels.
|
|
313
|
+
*/
|
|
314
|
+
export async function listForgeContainers() {
|
|
315
|
+
try {
|
|
316
|
+
const { stdout } = await execFile("docker", [
|
|
317
|
+
"ps",
|
|
318
|
+
"-a",
|
|
319
|
+
"--filter",
|
|
320
|
+
"label=forgeai.managed=true",
|
|
321
|
+
"--format",
|
|
322
|
+
"{{json .}}",
|
|
323
|
+
]);
|
|
324
|
+
const lines = stdout
|
|
325
|
+
.trim()
|
|
326
|
+
.split("\n")
|
|
327
|
+
.filter((line) => line.length > 0);
|
|
328
|
+
const containers = [];
|
|
329
|
+
for (const line of lines) {
|
|
330
|
+
try {
|
|
331
|
+
const data = JSON.parse(line);
|
|
332
|
+
const labels = {};
|
|
333
|
+
if (data.Labels) {
|
|
334
|
+
const pairs = data.Labels.split(",");
|
|
335
|
+
for (const pair of pairs) {
|
|
336
|
+
const [key, value] = pair.split("=");
|
|
337
|
+
if (key && value) {
|
|
338
|
+
labels[key] = value;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
containers.push({
|
|
343
|
+
id: data.ID,
|
|
344
|
+
name: data.Names || data.ID,
|
|
345
|
+
status: data.Status,
|
|
346
|
+
labels,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (parseErr) {
|
|
350
|
+
vmLog.warn("docker", `Failed to parse container JSON: ${line}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return containers;
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
vmLog.warn("docker", `Failed to list containers: ${String(error)}`);
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=docker-executor.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventEmitter-based proxy that wraps a ChildProcess from `docker exec` into AgentProcess.
|
|
3
|
+
*
|
|
4
|
+
* Unlike VmProcessProxy which communicates with the VM via vsock, this class
|
|
5
|
+
* directly wraps a local ChildProcess (from docker exec) and pipes its streams.
|
|
6
|
+
* The kill function is injected to avoid a dependency on SessionManager.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from "node:events";
|
|
9
|
+
import { PassThrough } from "node:stream";
|
|
10
|
+
import type { ChildProcess } from "node:child_process";
|
|
11
|
+
import type { AgentProcess } from "../orchestrator/connectors/connector.js";
|
|
12
|
+
export declare class DockerProcessProxy extends EventEmitter implements AgentProcess {
|
|
13
|
+
private readonly sessionId;
|
|
14
|
+
private readonly dockerExecProcess;
|
|
15
|
+
private readonly killFn;
|
|
16
|
+
readonly stdout: PassThrough;
|
|
17
|
+
readonly stderr: PassThrough;
|
|
18
|
+
pid: number;
|
|
19
|
+
exitCode: number | null;
|
|
20
|
+
killed: boolean;
|
|
21
|
+
/** Set to true when exit was caused by an authentication error */
|
|
22
|
+
isAuthError: boolean;
|
|
23
|
+
constructor(sessionId: string, dockerExecProcess: ChildProcess, killFn: (signal?: string) => Promise<void>);
|
|
24
|
+
/**
|
|
25
|
+
* Kill the agent process via the injected kill function.
|
|
26
|
+
* Returns true if the kill signal was sent, false if the process was already dead.
|
|
27
|
+
* Per AgentProcess interface, this must be synchronous (returns boolean, not Promise).
|
|
28
|
+
*/
|
|
29
|
+
kill(signal?: string): boolean;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=docker-process-proxy.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventEmitter-based proxy that wraps a ChildProcess from `docker exec` into AgentProcess.
|
|
3
|
+
*
|
|
4
|
+
* Unlike VmProcessProxy which communicates with the VM via vsock, this class
|
|
5
|
+
* directly wraps a local ChildProcess (from docker exec) and pipes its streams.
|
|
6
|
+
* The kill function is injected to avoid a dependency on SessionManager.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from "node:events";
|
|
9
|
+
import { PassThrough } from "node:stream";
|
|
10
|
+
import { vmLog } from "./log.js";
|
|
11
|
+
export class DockerProcessProxy extends EventEmitter {
|
|
12
|
+
sessionId;
|
|
13
|
+
dockerExecProcess;
|
|
14
|
+
killFn;
|
|
15
|
+
stdout;
|
|
16
|
+
stderr;
|
|
17
|
+
pid = -1;
|
|
18
|
+
exitCode = null;
|
|
19
|
+
killed = false;
|
|
20
|
+
/** Set to true when exit was caused by an authentication error */
|
|
21
|
+
isAuthError = false;
|
|
22
|
+
constructor(sessionId, dockerExecProcess, killFn) {
|
|
23
|
+
super();
|
|
24
|
+
this.sessionId = sessionId;
|
|
25
|
+
this.dockerExecProcess = dockerExecProcess;
|
|
26
|
+
this.killFn = killFn;
|
|
27
|
+
this.stdout = new PassThrough();
|
|
28
|
+
this.stderr = new PassThrough();
|
|
29
|
+
// Set PID from docker exec process
|
|
30
|
+
this.pid = dockerExecProcess.pid ?? -1;
|
|
31
|
+
// Pipe docker exec stdout/stderr to PassThrough streams
|
|
32
|
+
if (dockerExecProcess.stdout) {
|
|
33
|
+
dockerExecProcess.stdout.pipe(this.stdout);
|
|
34
|
+
}
|
|
35
|
+
if (dockerExecProcess.stderr) {
|
|
36
|
+
dockerExecProcess.stderr.pipe(this.stderr);
|
|
37
|
+
}
|
|
38
|
+
// Capture stderr and stdout for error logging
|
|
39
|
+
let stderrBuffer = "";
|
|
40
|
+
let stdoutBuffer = "";
|
|
41
|
+
if (dockerExecProcess.stderr) {
|
|
42
|
+
dockerExecProcess.stderr.on("data", (chunk) => {
|
|
43
|
+
stderrBuffer += chunk.toString("utf-8");
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (dockerExecProcess.stdout) {
|
|
47
|
+
dockerExecProcess.stdout.on("data", (chunk) => {
|
|
48
|
+
// Only keep last 1KB of stdout for error diagnosis
|
|
49
|
+
stdoutBuffer += chunk.toString("utf-8");
|
|
50
|
+
if (stdoutBuffer.length > 1024) {
|
|
51
|
+
stdoutBuffer = stdoutBuffer.slice(-1024);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Handle exit
|
|
56
|
+
dockerExecProcess.on("exit", (code, signal) => {
|
|
57
|
+
this.exitCode = code ?? (signal ? 128 : -1);
|
|
58
|
+
if (this.exitCode !== 0) {
|
|
59
|
+
const combined = (stderrBuffer.trim() +
|
|
60
|
+
" " +
|
|
61
|
+
stdoutBuffer.trim()).trim();
|
|
62
|
+
if (combined) {
|
|
63
|
+
// Always log the actual error first for debugging
|
|
64
|
+
vmLog.error("agent-stderr", `Session ${sessionId} exit=${this.exitCode}: ${combined.slice(0, 500)}`, this.sessionId);
|
|
65
|
+
// Flag as auth error on specific patterns:
|
|
66
|
+
// - stderr: explicit auth failures from Claude Code CLI
|
|
67
|
+
// - combined: authentication_error JSON from API (arrives via stdout stream-json)
|
|
68
|
+
const stderrOnly = stderrBuffer.trim();
|
|
69
|
+
const isAuthError = stderrOnly.includes("Not logged in") ||
|
|
70
|
+
stderrOnly.includes("Invalid or expired token") ||
|
|
71
|
+
stderrOnly.includes("CLAUDE_CODE_OAUTH_TOKEN") ||
|
|
72
|
+
combined.includes('"authentication_error"') ||
|
|
73
|
+
combined.includes("Failed to authenticate");
|
|
74
|
+
if (isAuthError) {
|
|
75
|
+
this.isAuthError = true;
|
|
76
|
+
vmLog.error("agent-auth", "Claude OAuth token expired or invalid. Run: forge auth setup-claude", this.sessionId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
vmLog.error("agent-stderr", `Session ${sessionId} exit=${this.exitCode} (no output captured)`, this.sessionId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this.emit("exit", this.exitCode, signal);
|
|
84
|
+
this.emit("close", this.exitCode, signal);
|
|
85
|
+
});
|
|
86
|
+
dockerExecProcess.on("error", (err) => {
|
|
87
|
+
this.emit("error", err);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Kill the agent process via the injected kill function.
|
|
92
|
+
* Returns true if the kill signal was sent, false if the process was already dead.
|
|
93
|
+
* Per AgentProcess interface, this must be synchronous (returns boolean, not Promise).
|
|
94
|
+
*/
|
|
95
|
+
kill(signal) {
|
|
96
|
+
if (this.exitCode !== null || this.killed)
|
|
97
|
+
return false;
|
|
98
|
+
this.killed = true;
|
|
99
|
+
this.killFn(signal ?? "SIGTERM").catch(() => {
|
|
100
|
+
/* best effort — process may already be gone */
|
|
101
|
+
});
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=docker-process-proxy.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* forge-vm-agent — In-VM daemon for ForgeAI agent execution.
|
|
4
|
+
*
|
|
5
|
+
* Runs inside the VM, listens on vsock for commands from the host.
|
|
6
|
+
* Spawns agent processes, multiplexes stdout/stderr back to host.
|
|
7
|
+
*
|
|
8
|
+
* This file is baked into the VM rootfs image.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=forge-vm-agent.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* forge-vm-agent — In-VM daemon for ForgeAI agent execution.
|
|
4
|
+
*
|
|
5
|
+
* Runs inside the VM, listens on vsock for commands from the host.
|
|
6
|
+
* Spawns agent processes, multiplexes stdout/stderr back to host.
|
|
7
|
+
*
|
|
8
|
+
* This file is baked into the VM rootfs image.
|
|
9
|
+
*/
|
|
10
|
+
import { createServer } from "node:net";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
const CHANNEL_CONTROL = 0;
|
|
13
|
+
const CHANNEL_STDOUT = 1;
|
|
14
|
+
const CHANNEL_STDERR = 2;
|
|
15
|
+
const CHANNEL_MCP = 3;
|
|
16
|
+
const HEADER_SIZE = 5;
|
|
17
|
+
const DISCONNECT_KILL_GRACE_MS = 5_000;
|
|
18
|
+
const VSOCK_PORT = parseInt(process.env.FORGE_VSOCK_PORT ?? "6789", 10);
|
|
19
|
+
let activeProcess = null;
|
|
20
|
+
let hostSocket = null;
|
|
21
|
+
function sendFrame(socket, channel, data) {
|
|
22
|
+
const payload = typeof data === "string" ? Buffer.from(data) : data;
|
|
23
|
+
const header = Buffer.alloc(HEADER_SIZE);
|
|
24
|
+
header[0] = channel;
|
|
25
|
+
header.writeUInt32BE(payload.length, 1);
|
|
26
|
+
socket.write(Buffer.concat([header, payload]));
|
|
27
|
+
}
|
|
28
|
+
function sendControl(socket, msg) {
|
|
29
|
+
sendFrame(socket, CHANNEL_CONTROL, JSON.stringify(msg));
|
|
30
|
+
}
|
|
31
|
+
function handleSpawn(socket, msg) {
|
|
32
|
+
if (activeProcess) {
|
|
33
|
+
sendControl(socket, { type: "error", message: "Agent already running" });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const ALLOWED_COMMANDS = new Set(["claude", "opencode"]);
|
|
37
|
+
if (!ALLOWED_COMMANDS.has(msg.command)) {
|
|
38
|
+
sendControl(socket, {
|
|
39
|
+
type: "error",
|
|
40
|
+
message: `Command not allowed: ${msg.command}`,
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const child = spawn(msg.command, msg.args, {
|
|
46
|
+
cwd: msg.cwd,
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
detached: false,
|
|
49
|
+
env: {
|
|
50
|
+
HOME: "/root",
|
|
51
|
+
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
52
|
+
LANG: process.env.LANG ?? "en_US.UTF-8",
|
|
53
|
+
TERM: process.env.TERM ?? "xterm-256color",
|
|
54
|
+
NODE_ENV: "production",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
if (child.pid === undefined) {
|
|
58
|
+
sendControl(socket, {
|
|
59
|
+
type: "error",
|
|
60
|
+
message: "Failed to spawn — no PID assigned",
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
activeProcess = child;
|
|
65
|
+
// Send PID to host
|
|
66
|
+
sendControl(socket, { type: "pid", pid: child.pid });
|
|
67
|
+
// Pipe stdout/stderr to host via vsock channels
|
|
68
|
+
child.stdout?.on("data", (data) => {
|
|
69
|
+
sendFrame(socket, CHANNEL_STDOUT, data);
|
|
70
|
+
});
|
|
71
|
+
child.stderr?.on("data", (data) => {
|
|
72
|
+
sendFrame(socket, CHANNEL_STDERR, data);
|
|
73
|
+
});
|
|
74
|
+
child.on("exit", (code) => {
|
|
75
|
+
sendControl(socket, { type: "exit", code: code ?? 1 });
|
|
76
|
+
activeProcess = null;
|
|
77
|
+
});
|
|
78
|
+
child.on("error", (err) => {
|
|
79
|
+
sendControl(socket, { type: "error", message: err.message });
|
|
80
|
+
activeProcess = null;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
sendControl(socket, { type: "error", message: `Spawn failed: ${message}` });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function handleKill(msg) {
|
|
89
|
+
if (!activeProcess)
|
|
90
|
+
return;
|
|
91
|
+
try {
|
|
92
|
+
const signal = msg.signal === "SIGKILL" ? "SIGKILL" : "SIGTERM";
|
|
93
|
+
activeProcess.kill(signal);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Best-effort kill
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function processBuffer(socket, buffer) {
|
|
100
|
+
while (buffer.data.length >= HEADER_SIZE) {
|
|
101
|
+
const channel = buffer.data[0];
|
|
102
|
+
const length = buffer.data.readUInt32BE(1);
|
|
103
|
+
if (buffer.data.length < HEADER_SIZE + length)
|
|
104
|
+
break;
|
|
105
|
+
const payload = buffer.data.subarray(HEADER_SIZE, HEADER_SIZE + length);
|
|
106
|
+
buffer.data = buffer.data.subarray(HEADER_SIZE + length);
|
|
107
|
+
if (channel === CHANNEL_CONTROL) {
|
|
108
|
+
try {
|
|
109
|
+
const msg = JSON.parse(payload.toString());
|
|
110
|
+
if (msg.type === "spawn")
|
|
111
|
+
handleSpawn(socket, msg);
|
|
112
|
+
else if (msg.type === "kill")
|
|
113
|
+
handleKill(msg);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Invalid control message, ignore
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// MCP channel forwarding could be added here
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Start vsock server (using TCP for development, AF_VSOCK in production)
|
|
123
|
+
const server = createServer((socket) => {
|
|
124
|
+
console.log("[forge-vm-agent] Host connected");
|
|
125
|
+
hostSocket = socket;
|
|
126
|
+
const buffer = { data: Buffer.alloc(0) };
|
|
127
|
+
socket.on("data", (data) => {
|
|
128
|
+
buffer.data = Buffer.concat([buffer.data, data]);
|
|
129
|
+
processBuffer(socket, buffer);
|
|
130
|
+
});
|
|
131
|
+
socket.on("close", () => {
|
|
132
|
+
console.log("[forge-vm-agent] Host disconnected");
|
|
133
|
+
hostSocket = null;
|
|
134
|
+
// Kill active process if host disconnects
|
|
135
|
+
if (activeProcess) {
|
|
136
|
+
activeProcess.kill("SIGTERM");
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
if (activeProcess && activeProcess.exitCode === null) {
|
|
139
|
+
activeProcess.kill("SIGKILL");
|
|
140
|
+
}
|
|
141
|
+
}, DISCONNECT_KILL_GRACE_MS);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
socket.on("error", (err) => {
|
|
145
|
+
console.error(`[forge-vm-agent] Socket error: ${err.message}`);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
server.listen(VSOCK_PORT, "127.0.0.1", () => {
|
|
149
|
+
console.log(`[forge-vm-agent] Listening on port ${VSOCK_PORT}`);
|
|
150
|
+
});
|
|
151
|
+
// Graceful shutdown
|
|
152
|
+
process.on("SIGTERM", () => {
|
|
153
|
+
if (activeProcess) {
|
|
154
|
+
activeProcess.kill("SIGTERM");
|
|
155
|
+
}
|
|
156
|
+
server.close();
|
|
157
|
+
process.exit(0);
|
|
158
|
+
});
|
|
159
|
+
//# sourceMappingURL=forge-vm-agent.js.map
|