@vm0/runner 2.2.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +765 -460
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -32,7 +32,10 @@ var runnerConfigSchema = z.object({
|
|
|
32
32
|
binary: z.string().min(1, "Firecracker binary path is required"),
|
|
33
33
|
kernel: z.string().min(1, "Kernel path is required"),
|
|
34
34
|
rootfs: z.string().min(1, "Rootfs path is required")
|
|
35
|
-
})
|
|
35
|
+
}),
|
|
36
|
+
proxy: z.object({
|
|
37
|
+
port: z.number().int().min(1024).max(65535).default(8080)
|
|
38
|
+
}).default({})
|
|
36
39
|
});
|
|
37
40
|
function loadConfig(configPath) {
|
|
38
41
|
if (!fs.existsSync(configPath)) {
|
|
@@ -135,7 +138,8 @@ async function completeJob(apiUrl, context, exitCode, error) {
|
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
// src/lib/executor.ts
|
|
138
|
-
import
|
|
141
|
+
import path4 from "path";
|
|
142
|
+
import fs6 from "fs";
|
|
139
143
|
|
|
140
144
|
// src/lib/firecracker/vm.ts
|
|
141
145
|
import { spawn } from "child_process";
|
|
@@ -153,7 +157,7 @@ var FirecrackerClient = class {
|
|
|
153
157
|
/**
|
|
154
158
|
* Make HTTP request to Firecracker API
|
|
155
159
|
*/
|
|
156
|
-
async request(method,
|
|
160
|
+
async request(method, path5, body) {
|
|
157
161
|
return new Promise((resolve, reject) => {
|
|
158
162
|
const bodyStr = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
159
163
|
const headers = {
|
|
@@ -166,11 +170,11 @@ var FirecrackerClient = class {
|
|
|
166
170
|
headers["Content-Length"] = Buffer.byteLength(bodyStr);
|
|
167
171
|
}
|
|
168
172
|
console.log(
|
|
169
|
-
`[FC API] ${method} ${
|
|
173
|
+
`[FC API] ${method} ${path5}${bodyStr ? ` (${Buffer.byteLength(bodyStr)} bytes)` : ""}`
|
|
170
174
|
);
|
|
171
175
|
const options = {
|
|
172
176
|
socketPath: this.socketPath,
|
|
173
|
-
path:
|
|
177
|
+
path: path5,
|
|
174
178
|
method,
|
|
175
179
|
headers,
|
|
176
180
|
// Disable agent to ensure fresh connection for each request
|
|
@@ -485,6 +489,52 @@ function checkNetworkPrerequisites() {
|
|
|
485
489
|
errors
|
|
486
490
|
};
|
|
487
491
|
}
|
|
492
|
+
async function setupVMProxyRules(vmIp, proxyPort) {
|
|
493
|
+
console.log(
|
|
494
|
+
`Setting up proxy rules for VM ${vmIp} -> localhost:${proxyPort}`
|
|
495
|
+
);
|
|
496
|
+
try {
|
|
497
|
+
await execCommand(
|
|
498
|
+
`iptables -t nat -C PREROUTING -s ${vmIp} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort}`
|
|
499
|
+
);
|
|
500
|
+
console.log(`Proxy rule for ${vmIp}:80 already exists`);
|
|
501
|
+
} catch {
|
|
502
|
+
await execCommand(
|
|
503
|
+
`iptables -t nat -A PREROUTING -s ${vmIp} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort}`
|
|
504
|
+
);
|
|
505
|
+
console.log(`Proxy rule for ${vmIp}:80 added`);
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
await execCommand(
|
|
509
|
+
`iptables -t nat -C PREROUTING -s ${vmIp} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort}`
|
|
510
|
+
);
|
|
511
|
+
console.log(`Proxy rule for ${vmIp}:443 already exists`);
|
|
512
|
+
} catch {
|
|
513
|
+
await execCommand(
|
|
514
|
+
`iptables -t nat -A PREROUTING -s ${vmIp} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort}`
|
|
515
|
+
);
|
|
516
|
+
console.log(`Proxy rule for ${vmIp}:443 added`);
|
|
517
|
+
}
|
|
518
|
+
console.log(`Proxy rules configured for VM ${vmIp}`);
|
|
519
|
+
}
|
|
520
|
+
async function removeVMProxyRules(vmIp, proxyPort) {
|
|
521
|
+
console.log(`Removing proxy rules for VM ${vmIp}...`);
|
|
522
|
+
try {
|
|
523
|
+
await execCommand(
|
|
524
|
+
`iptables -t nat -D PREROUTING -s ${vmIp} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort}`
|
|
525
|
+
);
|
|
526
|
+
console.log(`Proxy rule for ${vmIp}:80 removed`);
|
|
527
|
+
} catch {
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
await execCommand(
|
|
531
|
+
`iptables -t nat -D PREROUTING -s ${vmIp} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort}`
|
|
532
|
+
);
|
|
533
|
+
console.log(`Proxy rule for ${vmIp}:443 removed`);
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
console.log(`Proxy rules cleanup complete for VM ${vmIp}`);
|
|
537
|
+
}
|
|
488
538
|
|
|
489
539
|
// src/lib/firecracker/vm.ts
|
|
490
540
|
var FirecrackerVM = class {
|
|
@@ -1278,8 +1328,8 @@ function getErrorMap() {
|
|
|
1278
1328
|
return overrideErrorMap;
|
|
1279
1329
|
}
|
|
1280
1330
|
var makeIssue = (params) => {
|
|
1281
|
-
const { data, path:
|
|
1282
|
-
const fullPath = [...
|
|
1331
|
+
const { data, path: path5, errorMaps, issueData } = params;
|
|
1332
|
+
const fullPath = [...path5, ...issueData.path || []];
|
|
1283
1333
|
const fullIssue = {
|
|
1284
1334
|
...issueData,
|
|
1285
1335
|
path: fullPath
|
|
@@ -1378,11 +1428,11 @@ var errorUtil;
|
|
|
1378
1428
|
errorUtil2.toString = (message) => typeof message === "string" ? message : message === null || message === void 0 ? void 0 : message.message;
|
|
1379
1429
|
})(errorUtil || (errorUtil = {}));
|
|
1380
1430
|
var ParseInputLazyPath = class {
|
|
1381
|
-
constructor(parent, value,
|
|
1431
|
+
constructor(parent, value, path5, key) {
|
|
1382
1432
|
this._cachedPath = [];
|
|
1383
1433
|
this.parent = parent;
|
|
1384
1434
|
this.data = value;
|
|
1385
|
-
this._path =
|
|
1435
|
+
this._path = path5;
|
|
1386
1436
|
this._key = key;
|
|
1387
1437
|
}
|
|
1388
1438
|
get path() {
|
|
@@ -6106,7 +6156,9 @@ var executionContextSchema = z14.object({
|
|
|
6106
6156
|
environment: z14.record(z14.string(), z14.string()).nullable(),
|
|
6107
6157
|
resumeSession: resumeSessionSchema.nullable(),
|
|
6108
6158
|
secretValues: z14.array(z14.string()).nullable(),
|
|
6109
|
-
cliAgentType: z14.string()
|
|
6159
|
+
cliAgentType: z14.string(),
|
|
6160
|
+
// Network security mode flag
|
|
6161
|
+
experimentalNetworkSecurity: z14.boolean().optional()
|
|
6110
6162
|
});
|
|
6111
6163
|
var runnersJobClaimContract = c10.router({
|
|
6112
6164
|
claim: {
|
|
@@ -7855,415 +7907,6 @@ def final_telemetry_upload() -> bool:
|
|
|
7855
7907
|
return upload_telemetry()
|
|
7856
7908
|
`;
|
|
7857
7909
|
|
|
7858
|
-
// ../../packages/core/src/sandbox/scripts/lib/proxy_setup.py.ts
|
|
7859
|
-
var PROXY_SETUP_SCRIPT = `#!/usr/bin/env python3
|
|
7860
|
-
"""
|
|
7861
|
-
Proxy setup for VM0 network security mode.
|
|
7862
|
-
This script:
|
|
7863
|
-
1. Installs mitmproxy and dependencies
|
|
7864
|
-
2. Generates and installs CA certificate
|
|
7865
|
-
3. Configures nftables for transparent proxying
|
|
7866
|
-
4. Starts mitmproxy with the VM0 addon
|
|
7867
|
-
"""
|
|
7868
|
-
import os
|
|
7869
|
-
import sys
|
|
7870
|
-
import subprocess
|
|
7871
|
-
import time
|
|
7872
|
-
|
|
7873
|
-
# Add lib to path for imports
|
|
7874
|
-
sys.path.insert(0, "/usr/local/bin/vm0-agent/lib")
|
|
7875
|
-
|
|
7876
|
-
from log import log_info, log_error, log_warn
|
|
7877
|
-
|
|
7878
|
-
# Proxy configuration
|
|
7879
|
-
MITM_PORT = 8080
|
|
7880
|
-
MITM_CA_DIR = "/root/.mitmproxy" # Proxy setup runs as root
|
|
7881
|
-
MITM_CA_CERT = f"{MITM_CA_DIR}/mitmproxy-ca-cert.pem"
|
|
7882
|
-
ADDON_PATH = "/usr/local/bin/vm0-agent/lib/mitm_addon.py"
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
def run_cmd(cmd: list, check: bool = True) -> subprocess.CompletedProcess:
|
|
7886
|
-
"""Run a command and log output."""
|
|
7887
|
-
log_info(f"Running: {' '.join(cmd)}")
|
|
7888
|
-
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
7889
|
-
if result.stdout:
|
|
7890
|
-
log_info(f"stdout: {result.stdout.strip()}")
|
|
7891
|
-
if result.stderr:
|
|
7892
|
-
log_warn(f"stderr: {result.stderr.strip()}")
|
|
7893
|
-
if check and result.returncode != 0:
|
|
7894
|
-
raise RuntimeError(f"Command failed with code {result.returncode}")
|
|
7895
|
-
return result
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
def install_dependencies():
|
|
7899
|
-
"""Install required packages (must run as root)."""
|
|
7900
|
-
log_info("Installing dependencies...")
|
|
7901
|
-
|
|
7902
|
-
# Update apt cache
|
|
7903
|
-
run_cmd(["apt-get", "update", "-qq"])
|
|
7904
|
-
|
|
7905
|
-
# Install required packages
|
|
7906
|
-
run_cmd([
|
|
7907
|
-
"apt-get", "install", "-y", "-qq",
|
|
7908
|
-
"python3-pip",
|
|
7909
|
-
"nftables",
|
|
7910
|
-
"ca-certificates"
|
|
7911
|
-
])
|
|
7912
|
-
|
|
7913
|
-
# Install mitmproxy system-wide
|
|
7914
|
-
log_info("Installing mitmproxy (this may take a minute)...")
|
|
7915
|
-
run_cmd([
|
|
7916
|
-
"pip3", "install", "mitmproxy",
|
|
7917
|
-
"--break-system-packages",
|
|
7918
|
-
"--quiet"
|
|
7919
|
-
])
|
|
7920
|
-
|
|
7921
|
-
log_info("Dependencies installed successfully")
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
def setup_ca_certificate():
|
|
7925
|
-
"""Generate and install mitmproxy CA certificate."""
|
|
7926
|
-
log_info("Setting up CA certificate...")
|
|
7927
|
-
|
|
7928
|
-
# Create mitmproxy config directory
|
|
7929
|
-
os.makedirs(MITM_CA_DIR, exist_ok=True)
|
|
7930
|
-
|
|
7931
|
-
# Generate CA certificate by running mitmproxy briefly
|
|
7932
|
-
# This creates the CA cert if it doesn't exist
|
|
7933
|
-
log_info("Generating mitmproxy CA certificate...")
|
|
7934
|
-
proc = subprocess.Popen(
|
|
7935
|
-
["mitmdump", "--set", "confdir=" + MITM_CA_DIR],
|
|
7936
|
-
stdout=subprocess.PIPE,
|
|
7937
|
-
stderr=subprocess.PIPE
|
|
7938
|
-
)
|
|
7939
|
-
time.sleep(3) # Wait for cert generation
|
|
7940
|
-
proc.terminate()
|
|
7941
|
-
proc.wait()
|
|
7942
|
-
|
|
7943
|
-
# Verify cert was created
|
|
7944
|
-
if not os.path.exists(MITM_CA_CERT):
|
|
7945
|
-
raise RuntimeError("Failed to generate CA certificate")
|
|
7946
|
-
|
|
7947
|
-
# Install CA certificate system-wide
|
|
7948
|
-
log_info("Installing CA certificate system-wide...")
|
|
7949
|
-
|
|
7950
|
-
# Copy to system CA directory
|
|
7951
|
-
run_cmd([
|
|
7952
|
-
"cp", MITM_CA_CERT,
|
|
7953
|
-
"/usr/local/share/ca-certificates/mitmproxy-ca.crt"
|
|
7954
|
-
])
|
|
7955
|
-
|
|
7956
|
-
# Update CA certificates
|
|
7957
|
-
run_cmd(["update-ca-certificates"])
|
|
7958
|
-
|
|
7959
|
-
# Set environment for Python requests library
|
|
7960
|
-
os.environ["REQUESTS_CA_BUNDLE"] = "/etc/ssl/certs/ca-certificates.crt"
|
|
7961
|
-
os.environ["SSL_CERT_FILE"] = "/etc/ssl/certs/ca-certificates.crt"
|
|
7962
|
-
|
|
7963
|
-
log_info("CA certificate installed successfully")
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
def configure_nftables():
|
|
7967
|
-
"""Configure nftables for transparent proxying."""
|
|
7968
|
-
log_info("Configuring nftables for transparent proxy...")
|
|
7969
|
-
|
|
7970
|
-
# nftables rules for transparent proxy
|
|
7971
|
-
# - Skip traffic from root (UID 0) - mitmproxy runs as root
|
|
7972
|
-
# - Redirect all other TCP traffic to mitmproxy
|
|
7973
|
-
# NOTE: We use UID-based filtering because mitmproxy doesn't support SO_MARK
|
|
7974
|
-
nft_rules = f"""
|
|
7975
|
-
flush ruleset
|
|
7976
|
-
|
|
7977
|
-
table ip nat {{
|
|
7978
|
-
chain prerouting {{
|
|
7979
|
-
type nat hook prerouting priority -100;
|
|
7980
|
-
}}
|
|
7981
|
-
|
|
7982
|
-
chain output {{
|
|
7983
|
-
type nat hook output priority -100;
|
|
7984
|
-
|
|
7985
|
-
# Skip traffic from root (UID 0) - mitmproxy runs as root
|
|
7986
|
-
# This prevents redirect loop: mitmproxy -> nftables -> mitmproxy
|
|
7987
|
-
meta skuid 0 return
|
|
7988
|
-
|
|
7989
|
-
# Skip traffic to localhost
|
|
7990
|
-
ip daddr 127.0.0.0/8 return
|
|
7991
|
-
|
|
7992
|
-
# Skip traffic to private networks (internal communication)
|
|
7993
|
-
ip daddr 10.0.0.0/8 return
|
|
7994
|
-
ip daddr 172.16.0.0/12 return
|
|
7995
|
-
ip daddr 192.168.0.0/16 return
|
|
7996
|
-
|
|
7997
|
-
# Redirect HTTP traffic (port 80)
|
|
7998
|
-
tcp dport 80 redirect to :{MITM_PORT}
|
|
7999
|
-
|
|
8000
|
-
# Redirect HTTPS traffic (port 443)
|
|
8001
|
-
tcp dport 443 redirect to :{MITM_PORT}
|
|
8002
|
-
}}
|
|
8003
|
-
}}
|
|
8004
|
-
"""
|
|
8005
|
-
|
|
8006
|
-
# Write rules to file
|
|
8007
|
-
nft_file = "/tmp/vm0-proxy-rules.nft"
|
|
8008
|
-
with open(nft_file, "w") as f:
|
|
8009
|
-
f.write(nft_rules)
|
|
8010
|
-
|
|
8011
|
-
# Apply rules
|
|
8012
|
-
run_cmd(["nft", "-f", nft_file])
|
|
8013
|
-
|
|
8014
|
-
log_info("nftables configured successfully")
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
def start_mitmproxy():
|
|
8018
|
-
"""Start mitmproxy with the VM0 addon in background."""
|
|
8019
|
-
log_info("Starting mitmproxy...")
|
|
8020
|
-
|
|
8021
|
-
# Verify addon exists
|
|
8022
|
-
if not os.path.exists(ADDON_PATH):
|
|
8023
|
-
raise RuntimeError(f"Addon not found: {ADDON_PATH}")
|
|
8024
|
-
|
|
8025
|
-
# Start mitmproxy in transparent mode with addon
|
|
8026
|
-
# NOTE: mitmproxy runs as root, and nftables skips root's traffic (meta skuid 0)
|
|
8027
|
-
# to avoid redirect loop
|
|
8028
|
-
cmd = [
|
|
8029
|
-
"mitmdump",
|
|
8030
|
-
"--mode", "transparent",
|
|
8031
|
-
"--listen-port", str(MITM_PORT),
|
|
8032
|
-
"--set", f"confdir={MITM_CA_DIR}",
|
|
8033
|
-
"--scripts", ADDON_PATH,
|
|
8034
|
-
"--quiet" # Reduce log noise
|
|
8035
|
-
]
|
|
8036
|
-
|
|
8037
|
-
log_info(f"mitmproxy command: {' '.join(cmd)}")
|
|
8038
|
-
|
|
8039
|
-
# Start in background
|
|
8040
|
-
proc = subprocess.Popen(
|
|
8041
|
-
cmd,
|
|
8042
|
-
stdout=subprocess.DEVNULL,
|
|
8043
|
-
stderr=subprocess.DEVNULL,
|
|
8044
|
-
start_new_session=True
|
|
8045
|
-
)
|
|
8046
|
-
|
|
8047
|
-
# Wait briefly and check if it's running
|
|
8048
|
-
time.sleep(2)
|
|
8049
|
-
if proc.poll() is not None:
|
|
8050
|
-
raise RuntimeError("mitmproxy failed to start")
|
|
8051
|
-
|
|
8052
|
-
log_info(f"mitmproxy started (PID: {proc.pid})")
|
|
8053
|
-
|
|
8054
|
-
# Save PID for later cleanup if needed
|
|
8055
|
-
with open("/tmp/vm0-mitmproxy.pid", "w") as f:
|
|
8056
|
-
f.write(str(proc.pid))
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
def setup_proxy():
|
|
8060
|
-
"""Main setup function."""
|
|
8061
|
-
log_info("=== VM0 Proxy Setup Starting ===")
|
|
8062
|
-
start_time = time.time()
|
|
8063
|
-
|
|
8064
|
-
try:
|
|
8065
|
-
install_dependencies()
|
|
8066
|
-
setup_ca_certificate()
|
|
8067
|
-
configure_nftables()
|
|
8068
|
-
start_mitmproxy()
|
|
8069
|
-
|
|
8070
|
-
elapsed = time.time() - start_time
|
|
8071
|
-
log_info(f"=== Proxy Setup Complete ({elapsed:.1f}s) ===")
|
|
8072
|
-
return True
|
|
8073
|
-
|
|
8074
|
-
except Exception as e:
|
|
8075
|
-
log_error(f"Proxy setup failed: {e}")
|
|
8076
|
-
return False
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
if __name__ == "__main__":
|
|
8080
|
-
success = setup_proxy()
|
|
8081
|
-
sys.exit(0 if success else 1)
|
|
8082
|
-
`;
|
|
8083
|
-
|
|
8084
|
-
// ../../packages/core/src/sandbox/scripts/lib/mitm_addon.py.ts
|
|
8085
|
-
var MITM_ADDON_SCRIPT = `#!/usr/bin/env python3
|
|
8086
|
-
"""
|
|
8087
|
-
mitmproxy addon for VM0 network security mode.
|
|
8088
|
-
This addon:
|
|
8089
|
-
1. Intercepts all HTTPS requests
|
|
8090
|
-
2. Rewrites them to go through VM0 Proxy endpoint
|
|
8091
|
-
3. Preserves all original headers (including encrypted tokens)
|
|
8092
|
-
4. Logs network activity to JSONL file for observability
|
|
8093
|
-
"""
|
|
8094
|
-
import os
|
|
8095
|
-
import json
|
|
8096
|
-
import time
|
|
8097
|
-
import urllib.parse
|
|
8098
|
-
from mitmproxy import http, ctx
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
# VM0 Proxy configuration
|
|
8102
|
-
# API_URL is set by sandbox environment
|
|
8103
|
-
API_URL = os.environ.get("VM0_API_URL", "")
|
|
8104
|
-
API_TOKEN = os.environ.get("VM0_API_TOKEN", "")
|
|
8105
|
-
RUN_ID = os.environ.get("VM0_RUN_ID", "")
|
|
8106
|
-
VERCEL_BYPASS = os.environ.get("VERCEL_PROTECTION_BYPASS", "")
|
|
8107
|
-
|
|
8108
|
-
# Network log file path
|
|
8109
|
-
NETWORK_LOG_FILE = f"/tmp/vm0-network-{RUN_ID}.jsonl"
|
|
8110
|
-
|
|
8111
|
-
# Track request start times for latency calculation
|
|
8112
|
-
request_start_times = {}
|
|
8113
|
-
|
|
8114
|
-
# Construct proxy URL
|
|
8115
|
-
PROXY_URL = f"{API_URL}/api/webhooks/agent/proxy"
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
def log_network_entry(entry: dict) -> None:
|
|
8119
|
-
"""Write a network log entry to the JSONL file."""
|
|
8120
|
-
try:
|
|
8121
|
-
# Use O_CREAT | O_APPEND | O_WRONLY with mode 0o644 atomically
|
|
8122
|
-
# This avoids race conditions and ensures world-readable permissions
|
|
8123
|
-
# so the agent process (running as 'user') can read logs written by
|
|
8124
|
-
# mitmproxy (running as root)
|
|
8125
|
-
fd = os.open(NETWORK_LOG_FILE, os.O_CREAT | os.O_APPEND | os.O_WRONLY, 0o644)
|
|
8126
|
-
try:
|
|
8127
|
-
os.write(fd, (json.dumps(entry) + "\\n").encode())
|
|
8128
|
-
finally:
|
|
8129
|
-
os.close(fd)
|
|
8130
|
-
except Exception as e:
|
|
8131
|
-
ctx.log.warn(f"Failed to write network log: {e}")
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
def get_original_url(flow: http.HTTPFlow) -> str:
|
|
8135
|
-
"""Reconstruct the original target URL from the request."""
|
|
8136
|
-
scheme = "https" if flow.request.port == 443 else "http"
|
|
8137
|
-
# Use pretty_host which prefers Host header over IP in transparent proxy mode
|
|
8138
|
-
# This is critical because flow.request.host returns the destination IP address
|
|
8139
|
-
# in transparent mode, but SSL certificates are issued for hostnames
|
|
8140
|
-
host = flow.request.pretty_host
|
|
8141
|
-
port = flow.request.port
|
|
8142
|
-
|
|
8143
|
-
# Include port in URL only if non-standard
|
|
8144
|
-
if (scheme == "https" and port != 443) or (scheme == "http" and port != 80):
|
|
8145
|
-
host_with_port = f"{host}:{port}"
|
|
8146
|
-
else:
|
|
8147
|
-
host_with_port = host
|
|
8148
|
-
|
|
8149
|
-
# Reconstruct full URL with path and query
|
|
8150
|
-
path = flow.request.path
|
|
8151
|
-
return f"{scheme}://{host_with_port}{path}"
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
def request(flow: http.HTTPFlow) -> None:
|
|
8155
|
-
"""
|
|
8156
|
-
Intercept request and rewrite to VM0 Proxy.
|
|
8157
|
-
|
|
8158
|
-
Original request:
|
|
8159
|
-
POST https://api.anthropic.com/v1/messages
|
|
8160
|
-
Headers: x-api-key: vm0_enc_xxx, Content-Type: application/json
|
|
8161
|
-
Body: {...}
|
|
8162
|
-
|
|
8163
|
-
Rewritten to:
|
|
8164
|
-
POST https://vm0.ai/api/webhooks/agent/proxy?url=https%3A%2F%2Fapi.anthropic.com%2Fv1%2Fmessages&runId=xxx
|
|
8165
|
-
Headers: Authorization: Bearer vm0_live_xxx, x-api-key: vm0_enc_xxx, Content-Type: application/json
|
|
8166
|
-
Body: {...}
|
|
8167
|
-
"""
|
|
8168
|
-
# Track request start time for latency calculation
|
|
8169
|
-
request_start_times[flow.id] = time.time()
|
|
8170
|
-
|
|
8171
|
-
# Skip if no API URL configured
|
|
8172
|
-
if not API_URL:
|
|
8173
|
-
ctx.log.warn("VM0_API_URL not set, passing through")
|
|
8174
|
-
return
|
|
8175
|
-
|
|
8176
|
-
# Skip rewriting requests already going to VM0 (avoid loops)
|
|
8177
|
-
# But still allow them to be logged in the response handler
|
|
8178
|
-
if API_URL in flow.request.pretty_url:
|
|
8179
|
-
# Store original URL for logging
|
|
8180
|
-
flow.metadata["original_url"] = flow.request.pretty_url
|
|
8181
|
-
flow.metadata["skip_rewrite"] = True
|
|
8182
|
-
return
|
|
8183
|
-
|
|
8184
|
-
# Get original target URL
|
|
8185
|
-
original_url = get_original_url(flow)
|
|
8186
|
-
|
|
8187
|
-
# Store original URL for logging in response handler
|
|
8188
|
-
flow.metadata["original_url"] = original_url
|
|
8189
|
-
|
|
8190
|
-
ctx.log.info(f"Proxying: {original_url} -> VM0 Proxy")
|
|
8191
|
-
|
|
8192
|
-
# Parse proxy URL
|
|
8193
|
-
parsed = urllib.parse.urlparse(PROXY_URL)
|
|
8194
|
-
|
|
8195
|
-
# Build query params properly using urlencode
|
|
8196
|
-
query_params = {"url": original_url}
|
|
8197
|
-
if RUN_ID:
|
|
8198
|
-
query_params["runId"] = RUN_ID
|
|
8199
|
-
query_string = urllib.parse.urlencode(query_params)
|
|
8200
|
-
|
|
8201
|
-
# Rewrite request to proxy
|
|
8202
|
-
flow.request.host = parsed.hostname
|
|
8203
|
-
flow.request.port = 443 if parsed.scheme == "https" else 80
|
|
8204
|
-
flow.request.scheme = parsed.scheme
|
|
8205
|
-
flow.request.path = f"{parsed.path}?{query_string}"
|
|
8206
|
-
|
|
8207
|
-
# Save original Authorization header before overwriting (for transparent proxy)
|
|
8208
|
-
# VM0 Proxy will restore this and decrypt any proxy tokens
|
|
8209
|
-
if "Authorization" in flow.request.headers:
|
|
8210
|
-
flow.request.headers["x-vm0-original-authorization"] = flow.request.headers["Authorization"]
|
|
8211
|
-
|
|
8212
|
-
# Add sandbox authentication token
|
|
8213
|
-
if API_TOKEN:
|
|
8214
|
-
flow.request.headers["Authorization"] = f"Bearer {API_TOKEN}"
|
|
8215
|
-
|
|
8216
|
-
# Add Vercel bypass header if configured
|
|
8217
|
-
if VERCEL_BYPASS:
|
|
8218
|
-
flow.request.headers["x-vercel-protection-bypass"] = VERCEL_BYPASS
|
|
8219
|
-
|
|
8220
|
-
# All other headers (including x-api-key with vm0_enc_xxx) are preserved
|
|
8221
|
-
# The proxy endpoint will decrypt the token before forwarding
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
def response(flow: http.HTTPFlow) -> None:
|
|
8225
|
-
"""
|
|
8226
|
-
Handle response from VM0 Proxy.
|
|
8227
|
-
Log network activity and any errors for debugging.
|
|
8228
|
-
"""
|
|
8229
|
-
# Calculate latency
|
|
8230
|
-
start_time = request_start_times.pop(flow.id, None)
|
|
8231
|
-
latency_ms = int((time.time() - start_time) * 1000) if start_time else 0
|
|
8232
|
-
|
|
8233
|
-
# Get original URL (stored in request handler) or use current URL
|
|
8234
|
-
original_url = flow.metadata.get("original_url", flow.request.pretty_url)
|
|
8235
|
-
|
|
8236
|
-
# Calculate request/response sizes
|
|
8237
|
-
request_size = len(flow.request.content) if flow.request.content else 0
|
|
8238
|
-
response_size = len(flow.response.content) if flow.response and flow.response.content else 0
|
|
8239
|
-
|
|
8240
|
-
# Determine status code
|
|
8241
|
-
status_code = flow.response.status_code if flow.response else 0
|
|
8242
|
-
|
|
8243
|
-
# Log network entry
|
|
8244
|
-
log_entry = {
|
|
8245
|
-
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()),
|
|
8246
|
-
"method": flow.request.method,
|
|
8247
|
-
"url": original_url,
|
|
8248
|
-
"status": status_code,
|
|
8249
|
-
"latency_ms": latency_ms,
|
|
8250
|
-
"request_size": request_size,
|
|
8251
|
-
"response_size": response_size,
|
|
8252
|
-
}
|
|
8253
|
-
log_network_entry(log_entry)
|
|
8254
|
-
|
|
8255
|
-
# Log errors to mitmproxy console
|
|
8256
|
-
if flow.response and flow.response.status_code >= 400:
|
|
8257
|
-
ctx.log.warn(
|
|
8258
|
-
f"Proxy response {flow.response.status_code}: "
|
|
8259
|
-
f"{original_url}"
|
|
8260
|
-
)
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
# mitmproxy addon registration
|
|
8264
|
-
addons = [request, response]
|
|
8265
|
-
`;
|
|
8266
|
-
|
|
8267
7910
|
// ../../packages/core/src/sandbox/scripts/lib/secret_masker.py.ts
|
|
8268
7911
|
var SECRET_MASKER_SCRIPT = `#!/usr/bin/env python3
|
|
8269
7912
|
"""
|
|
@@ -8852,8 +8495,6 @@ var SCRIPT_PATHS = {
|
|
|
8852
8495
|
mockClaude: "/usr/local/bin/vm0-agent/lib/mock_claude.py",
|
|
8853
8496
|
metrics: "/usr/local/bin/vm0-agent/lib/metrics.py",
|
|
8854
8497
|
uploadTelemetry: "/usr/local/bin/vm0-agent/lib/upload_telemetry.py",
|
|
8855
|
-
proxySetup: "/usr/local/bin/vm0-agent/lib/proxy_setup.py",
|
|
8856
|
-
mitmAddon: "/usr/local/bin/vm0-agent/lib/mitm_addon.py",
|
|
8857
8498
|
secretMasker: "/usr/local/bin/vm0-agent/lib/secret_masker.py"
|
|
8858
8499
|
};
|
|
8859
8500
|
|
|
@@ -8927,8 +8568,6 @@ function getAllScripts() {
|
|
|
8927
8568
|
{ content: MOCK_CLAUDE_SCRIPT, path: SCRIPT_PATHS.mockClaude },
|
|
8928
8569
|
{ content: METRICS_SCRIPT, path: SCRIPT_PATHS.metrics },
|
|
8929
8570
|
{ content: UPLOAD_TELEMETRY_SCRIPT, path: SCRIPT_PATHS.uploadTelemetry },
|
|
8930
|
-
{ content: PROXY_SETUP_SCRIPT, path: SCRIPT_PATHS.proxySetup },
|
|
8931
|
-
{ content: MITM_ADDON_SCRIPT, path: SCRIPT_PATHS.mitmAddon },
|
|
8932
8571
|
{ content: SECRET_MASKER_SCRIPT, path: SCRIPT_PATHS.secretMasker },
|
|
8933
8572
|
{ content: RUN_AGENT_SCRIPT, path: SCRIPT_PATHS.runAgent },
|
|
8934
8573
|
// Env loader is runner-specific (loads env from JSON before executing run-agent.py)
|
|
@@ -8936,42 +8575,572 @@ function getAllScripts() {
|
|
|
8936
8575
|
];
|
|
8937
8576
|
}
|
|
8938
8577
|
|
|
8939
|
-
// src/lib/
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
VM0_PROMPT: context.prompt,
|
|
8949
|
-
VM0_WORKING_DIR: context.workingDir,
|
|
8950
|
-
CLI_AGENT_TYPE: context.cliAgentType || "claude-code"
|
|
8951
|
-
};
|
|
8952
|
-
const vercelBypass = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
8953
|
-
if (vercelBypass) {
|
|
8954
|
-
envVars.VERCEL_PROTECTION_BYPASS = vercelBypass;
|
|
8955
|
-
}
|
|
8956
|
-
const useMockClaude = process.env.USE_MOCK_CLAUDE;
|
|
8957
|
-
if (useMockClaude) {
|
|
8958
|
-
envVars.USE_MOCK_CLAUDE = useMockClaude;
|
|
8578
|
+
// src/lib/proxy/vm-registry.ts
|
|
8579
|
+
import fs4 from "fs";
|
|
8580
|
+
var DEFAULT_REGISTRY_PATH = "/tmp/vm0-vm-registry.json";
|
|
8581
|
+
var VMRegistry = class {
|
|
8582
|
+
registryPath;
|
|
8583
|
+
data;
|
|
8584
|
+
constructor(registryPath = DEFAULT_REGISTRY_PATH) {
|
|
8585
|
+
this.registryPath = registryPath;
|
|
8586
|
+
this.data = this.load();
|
|
8959
8587
|
}
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8588
|
+
/**
|
|
8589
|
+
* Load registry data from file
|
|
8590
|
+
*/
|
|
8591
|
+
load() {
|
|
8592
|
+
try {
|
|
8593
|
+
if (fs4.existsSync(this.registryPath)) {
|
|
8594
|
+
const content = fs4.readFileSync(this.registryPath, "utf-8");
|
|
8595
|
+
return JSON.parse(content);
|
|
8596
|
+
}
|
|
8597
|
+
} catch {
|
|
8598
|
+
}
|
|
8599
|
+
return { vms: {}, updatedAt: Date.now() };
|
|
8969
8600
|
}
|
|
8970
|
-
|
|
8971
|
-
|
|
8601
|
+
/**
|
|
8602
|
+
* Save registry data to file atomically
|
|
8603
|
+
*/
|
|
8604
|
+
save() {
|
|
8605
|
+
this.data.updatedAt = Date.now();
|
|
8606
|
+
const content = JSON.stringify(this.data, null, 2);
|
|
8607
|
+
const tempPath = `${this.registryPath}.tmp`;
|
|
8608
|
+
fs4.writeFileSync(tempPath, content, { mode: 420 });
|
|
8609
|
+
fs4.renameSync(tempPath, this.registryPath);
|
|
8972
8610
|
}
|
|
8973
|
-
|
|
8974
|
-
|
|
8611
|
+
/**
|
|
8612
|
+
* Register a VM with its IP address
|
|
8613
|
+
*/
|
|
8614
|
+
register(vmIp, runId, sandboxToken) {
|
|
8615
|
+
this.data.vms[vmIp] = {
|
|
8616
|
+
runId,
|
|
8617
|
+
sandboxToken,
|
|
8618
|
+
registeredAt: Date.now()
|
|
8619
|
+
};
|
|
8620
|
+
this.save();
|
|
8621
|
+
console.log(`[VMRegistry] Registered VM ${vmIp} for run ${runId}`);
|
|
8622
|
+
}
|
|
8623
|
+
/**
|
|
8624
|
+
* Unregister a VM by IP address
|
|
8625
|
+
*/
|
|
8626
|
+
unregister(vmIp) {
|
|
8627
|
+
if (this.data.vms[vmIp]) {
|
|
8628
|
+
const registration = this.data.vms[vmIp];
|
|
8629
|
+
delete this.data.vms[vmIp];
|
|
8630
|
+
this.save();
|
|
8631
|
+
console.log(
|
|
8632
|
+
`[VMRegistry] Unregistered VM ${vmIp} (run ${registration.runId})`
|
|
8633
|
+
);
|
|
8634
|
+
}
|
|
8635
|
+
}
|
|
8636
|
+
/**
|
|
8637
|
+
* Look up registration by VM IP
|
|
8638
|
+
*/
|
|
8639
|
+
lookup(vmIp) {
|
|
8640
|
+
return this.data.vms[vmIp];
|
|
8641
|
+
}
|
|
8642
|
+
/**
|
|
8643
|
+
* Get all registered VMs
|
|
8644
|
+
*/
|
|
8645
|
+
getAll() {
|
|
8646
|
+
return { ...this.data.vms };
|
|
8647
|
+
}
|
|
8648
|
+
/**
|
|
8649
|
+
* Clear all registrations
|
|
8650
|
+
*/
|
|
8651
|
+
clear() {
|
|
8652
|
+
this.data.vms = {};
|
|
8653
|
+
this.save();
|
|
8654
|
+
console.log("[VMRegistry] Cleared all registrations");
|
|
8655
|
+
}
|
|
8656
|
+
/**
|
|
8657
|
+
* Get the path to the registry file
|
|
8658
|
+
*/
|
|
8659
|
+
getRegistryPath() {
|
|
8660
|
+
return this.registryPath;
|
|
8661
|
+
}
|
|
8662
|
+
};
|
|
8663
|
+
var globalRegistry = null;
|
|
8664
|
+
function getVMRegistry() {
|
|
8665
|
+
if (!globalRegistry) {
|
|
8666
|
+
globalRegistry = new VMRegistry();
|
|
8667
|
+
}
|
|
8668
|
+
return globalRegistry;
|
|
8669
|
+
}
|
|
8670
|
+
function initVMRegistry(registryPath) {
|
|
8671
|
+
globalRegistry = new VMRegistry(registryPath);
|
|
8672
|
+
return globalRegistry;
|
|
8673
|
+
}
|
|
8674
|
+
|
|
8675
|
+
// src/lib/proxy/proxy-manager.ts
|
|
8676
|
+
import { spawn as spawn2 } from "child_process";
|
|
8677
|
+
import fs5 from "fs";
|
|
8678
|
+
import path3 from "path";
|
|
8679
|
+
|
|
8680
|
+
// src/lib/proxy/mitm-addon-script.ts
|
|
8681
|
+
var RUNNER_MITM_ADDON_SCRIPT = `#!/usr/bin/env python3
|
|
8682
|
+
"""
|
|
8683
|
+
mitmproxy addon for VM0 runner-level network security mode.
|
|
8684
|
+
|
|
8685
|
+
This addon runs on the runner HOST (not inside VMs) and:
|
|
8686
|
+
1. Intercepts all HTTPS requests from VMs
|
|
8687
|
+
2. Looks up the source VM's runId from the VM registry
|
|
8688
|
+
3. Rewrites requests to go through VM0 Proxy endpoint
|
|
8689
|
+
4. Preserves all original headers (including encrypted tokens)
|
|
8690
|
+
5. Logs network activity per-run to JSONL files
|
|
8691
|
+
"""
|
|
8692
|
+
import os
|
|
8693
|
+
import json
|
|
8694
|
+
import time
|
|
8695
|
+
import urllib.parse
|
|
8696
|
+
from mitmproxy import http, ctx
|
|
8697
|
+
|
|
8698
|
+
|
|
8699
|
+
# VM0 Proxy configuration from environment
|
|
8700
|
+
API_URL = os.environ.get("VM0_API_URL", "https://www.vm0.ai")
|
|
8701
|
+
REGISTRY_PATH = os.environ.get("VM0_REGISTRY_PATH", "/tmp/vm0-vm-registry.json")
|
|
8702
|
+
VERCEL_BYPASS = os.environ.get("VERCEL_PROTECTION_BYPASS", "")
|
|
8703
|
+
|
|
8704
|
+
# Construct proxy URL
|
|
8705
|
+
PROXY_URL = f"{API_URL}/api/webhooks/agent/proxy"
|
|
8706
|
+
|
|
8707
|
+
# Cache for VM registry (reloaded periodically)
|
|
8708
|
+
_registry_cache = {}
|
|
8709
|
+
_registry_cache_time = 0
|
|
8710
|
+
REGISTRY_CACHE_TTL = 2 # seconds
|
|
8711
|
+
|
|
8712
|
+
# Track request start times for latency calculation
|
|
8713
|
+
request_start_times = {}
|
|
8714
|
+
|
|
8715
|
+
|
|
8716
|
+
def load_registry() -> dict:
|
|
8717
|
+
"""Load the VM registry from file, with caching."""
|
|
8718
|
+
global _registry_cache, _registry_cache_time
|
|
8719
|
+
|
|
8720
|
+
now = time.time()
|
|
8721
|
+
if now - _registry_cache_time < REGISTRY_CACHE_TTL:
|
|
8722
|
+
return _registry_cache
|
|
8723
|
+
|
|
8724
|
+
try:
|
|
8725
|
+
if os.path.exists(REGISTRY_PATH):
|
|
8726
|
+
with open(REGISTRY_PATH, "r") as f:
|
|
8727
|
+
data = json.load(f)
|
|
8728
|
+
_registry_cache = data.get("vms", {})
|
|
8729
|
+
_registry_cache_time = now
|
|
8730
|
+
return _registry_cache
|
|
8731
|
+
except Exception as e:
|
|
8732
|
+
ctx.log.warn(f"Failed to load VM registry: {e}")
|
|
8733
|
+
|
|
8734
|
+
return _registry_cache
|
|
8735
|
+
|
|
8736
|
+
|
|
8737
|
+
def get_vm_info(client_ip: str) -> dict | None:
|
|
8738
|
+
"""Look up VM info by client IP address."""
|
|
8739
|
+
registry = load_registry()
|
|
8740
|
+
return registry.get(client_ip)
|
|
8741
|
+
|
|
8742
|
+
|
|
8743
|
+
def get_network_log_path(run_id: str) -> str:
|
|
8744
|
+
"""Get the network log file path for a run."""
|
|
8745
|
+
return f"/tmp/vm0-network-{run_id}.jsonl"
|
|
8746
|
+
|
|
8747
|
+
|
|
8748
|
+
def log_network_entry(run_id: str, entry: dict) -> None:
|
|
8749
|
+
"""Write a network log entry to the per-run JSONL file."""
|
|
8750
|
+
if not run_id:
|
|
8751
|
+
return
|
|
8752
|
+
|
|
8753
|
+
log_path = get_network_log_path(run_id)
|
|
8754
|
+
try:
|
|
8755
|
+
fd = os.open(log_path, os.O_CREAT | os.O_APPEND | os.O_WRONLY, 0o644)
|
|
8756
|
+
try:
|
|
8757
|
+
os.write(fd, (json.dumps(entry) + "\\n").encode())
|
|
8758
|
+
finally:
|
|
8759
|
+
os.close(fd)
|
|
8760
|
+
except Exception as e:
|
|
8761
|
+
ctx.log.warn(f"Failed to write network log: {e}")
|
|
8762
|
+
|
|
8763
|
+
|
|
8764
|
+
def get_original_url(flow: http.HTTPFlow) -> str:
|
|
8765
|
+
"""Reconstruct the original target URL from the request."""
|
|
8766
|
+
scheme = "https" if flow.request.port == 443 else "http"
|
|
8767
|
+
host = flow.request.pretty_host
|
|
8768
|
+
port = flow.request.port
|
|
8769
|
+
|
|
8770
|
+
if (scheme == "https" and port != 443) or (scheme == "http" and port != 80):
|
|
8771
|
+
host_with_port = f"{host}:{port}"
|
|
8772
|
+
else:
|
|
8773
|
+
host_with_port = host
|
|
8774
|
+
|
|
8775
|
+
path = flow.request.path
|
|
8776
|
+
return f"{scheme}://{host_with_port}{path}"
|
|
8777
|
+
|
|
8778
|
+
|
|
8779
|
+
def request(flow: http.HTTPFlow) -> None:
|
|
8780
|
+
"""
|
|
8781
|
+
Intercept request and rewrite to VM0 Proxy.
|
|
8782
|
+
|
|
8783
|
+
Identifies the source VM by client IP and looks up the associated
|
|
8784
|
+
runId and sandboxToken from the VM registry.
|
|
8785
|
+
"""
|
|
8786
|
+
# Track request start time
|
|
8787
|
+
request_start_times[flow.id] = time.time()
|
|
8788
|
+
|
|
8789
|
+
# Get client IP (source VM)
|
|
8790
|
+
client_ip = flow.client_conn.peername[0] if flow.client_conn.peername else None
|
|
8791
|
+
|
|
8792
|
+
if not client_ip:
|
|
8793
|
+
ctx.log.warn("No client IP available, passing through")
|
|
8794
|
+
return
|
|
8795
|
+
|
|
8796
|
+
# Look up VM info from registry
|
|
8797
|
+
vm_info = get_vm_info(client_ip)
|
|
8798
|
+
|
|
8799
|
+
if not vm_info:
|
|
8800
|
+
# Not a registered VM, pass through without proxying
|
|
8801
|
+
# This allows non-VM traffic to work normally
|
|
8802
|
+
ctx.log.info(f"No VM registration for {client_ip}, passing through")
|
|
8803
|
+
return
|
|
8804
|
+
|
|
8805
|
+
run_id = vm_info.get("runId", "")
|
|
8806
|
+
sandbox_token = vm_info.get("sandboxToken", "")
|
|
8807
|
+
|
|
8808
|
+
# Store info for response handler
|
|
8809
|
+
flow.metadata["vm_run_id"] = run_id
|
|
8810
|
+
flow.metadata["vm_client_ip"] = client_ip
|
|
8811
|
+
|
|
8812
|
+
# Skip if no API URL configured
|
|
8813
|
+
if not API_URL:
|
|
8814
|
+
ctx.log.warn("VM0_API_URL not set, passing through")
|
|
8815
|
+
return
|
|
8816
|
+
|
|
8817
|
+
# Skip rewriting requests already going to VM0 (avoid loops)
|
|
8818
|
+
if API_URL in flow.request.pretty_url:
|
|
8819
|
+
flow.metadata["original_url"] = flow.request.pretty_url
|
|
8820
|
+
flow.metadata["skip_rewrite"] = True
|
|
8821
|
+
return
|
|
8822
|
+
|
|
8823
|
+
# Skip rewriting requests to trusted domains (S3, etc.)
|
|
8824
|
+
# S3 presigned URLs have signatures that break when proxied
|
|
8825
|
+
host = flow.request.pretty_host.lower()
|
|
8826
|
+
TRUSTED_DOMAINS = [
|
|
8827
|
+
".s3.amazonaws.com",
|
|
8828
|
+
".s3-", # Regional S3 endpoints like s3-us-west-2.amazonaws.com
|
|
8829
|
+
"s3.amazonaws.com",
|
|
8830
|
+
".r2.cloudflarestorage.com",
|
|
8831
|
+
".storage.googleapis.com",
|
|
8832
|
+
]
|
|
8833
|
+
for domain in TRUSTED_DOMAINS:
|
|
8834
|
+
if domain in host or host.endswith(domain.lstrip(".")):
|
|
8835
|
+
ctx.log.info(f"[{run_id}] Skipping trusted domain: {host}")
|
|
8836
|
+
flow.metadata["original_url"] = get_original_url(flow)
|
|
8837
|
+
flow.metadata["skip_rewrite"] = True
|
|
8838
|
+
return
|
|
8839
|
+
|
|
8840
|
+
# Get original target URL
|
|
8841
|
+
original_url = get_original_url(flow)
|
|
8842
|
+
flow.metadata["original_url"] = original_url
|
|
8843
|
+
|
|
8844
|
+
ctx.log.info(f"[{run_id}] Proxying: {original_url}")
|
|
8845
|
+
|
|
8846
|
+
# Parse proxy URL
|
|
8847
|
+
parsed = urllib.parse.urlparse(PROXY_URL)
|
|
8848
|
+
|
|
8849
|
+
# Build query params
|
|
8850
|
+
query_params = {"url": original_url}
|
|
8851
|
+
if run_id:
|
|
8852
|
+
query_params["runId"] = run_id
|
|
8853
|
+
query_string = urllib.parse.urlencode(query_params)
|
|
8854
|
+
|
|
8855
|
+
# Rewrite request to proxy
|
|
8856
|
+
flow.request.host = parsed.hostname
|
|
8857
|
+
flow.request.port = 443 if parsed.scheme == "https" else 80
|
|
8858
|
+
flow.request.scheme = parsed.scheme
|
|
8859
|
+
flow.request.path = f"{parsed.path}?{query_string}"
|
|
8860
|
+
|
|
8861
|
+
# Save original Authorization header before overwriting
|
|
8862
|
+
if "Authorization" in flow.request.headers:
|
|
8863
|
+
flow.request.headers["x-vm0-original-authorization"] = flow.request.headers["Authorization"]
|
|
8864
|
+
|
|
8865
|
+
# Add sandbox authentication token
|
|
8866
|
+
if sandbox_token:
|
|
8867
|
+
flow.request.headers["Authorization"] = f"Bearer {sandbox_token}"
|
|
8868
|
+
|
|
8869
|
+
# Add Vercel bypass header if configured
|
|
8870
|
+
if VERCEL_BYPASS:
|
|
8871
|
+
flow.request.headers["x-vercel-protection-bypass"] = VERCEL_BYPASS
|
|
8872
|
+
|
|
8873
|
+
|
|
8874
|
+
def response(flow: http.HTTPFlow) -> None:
|
|
8875
|
+
"""
|
|
8876
|
+
Handle response and log network activity.
|
|
8877
|
+
"""
|
|
8878
|
+
# Calculate latency
|
|
8879
|
+
start_time = request_start_times.pop(flow.id, None)
|
|
8880
|
+
latency_ms = int((time.time() - start_time) * 1000) if start_time else 0
|
|
8881
|
+
|
|
8882
|
+
# Get stored info
|
|
8883
|
+
run_id = flow.metadata.get("vm_run_id", "")
|
|
8884
|
+
original_url = flow.metadata.get("original_url", flow.request.pretty_url)
|
|
8885
|
+
|
|
8886
|
+
# Calculate sizes
|
|
8887
|
+
request_size = len(flow.request.content) if flow.request.content else 0
|
|
8888
|
+
response_size = len(flow.response.content) if flow.response and flow.response.content else 0
|
|
8889
|
+
status_code = flow.response.status_code if flow.response else 0
|
|
8890
|
+
|
|
8891
|
+
# Log network entry for this run
|
|
8892
|
+
if run_id:
|
|
8893
|
+
log_entry = {
|
|
8894
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()),
|
|
8895
|
+
"method": flow.request.method,
|
|
8896
|
+
"url": original_url,
|
|
8897
|
+
"status": status_code,
|
|
8898
|
+
"latency_ms": latency_ms,
|
|
8899
|
+
"request_size": request_size,
|
|
8900
|
+
"response_size": response_size,
|
|
8901
|
+
}
|
|
8902
|
+
log_network_entry(run_id, log_entry)
|
|
8903
|
+
|
|
8904
|
+
# Log errors to mitmproxy console
|
|
8905
|
+
if flow.response and flow.response.status_code >= 400:
|
|
8906
|
+
ctx.log.warn(
|
|
8907
|
+
f"[{run_id}] Proxy response {flow.response.status_code}: {original_url}"
|
|
8908
|
+
)
|
|
8909
|
+
|
|
8910
|
+
|
|
8911
|
+
# mitmproxy addon registration
|
|
8912
|
+
addons = [request, response]
|
|
8913
|
+
`;
|
|
8914
|
+
|
|
8915
|
+
// src/lib/proxy/proxy-manager.ts
|
|
8916
|
+
var DEFAULT_PROXY_CONFIG = {
|
|
8917
|
+
port: 8080,
|
|
8918
|
+
caDir: "/opt/vm0-runner/proxy",
|
|
8919
|
+
addonPath: "/opt/vm0-runner/proxy/mitm_addon.py",
|
|
8920
|
+
registryPath: DEFAULT_REGISTRY_PATH,
|
|
8921
|
+
apiUrl: process.env.VM0_API_URL || "https://www.vm0.ai"
|
|
8922
|
+
};
|
|
8923
|
+
var ProxyManager = class {
|
|
8924
|
+
config;
|
|
8925
|
+
process = null;
|
|
8926
|
+
isRunning = false;
|
|
8927
|
+
constructor(config = {}) {
|
|
8928
|
+
this.config = { ...DEFAULT_PROXY_CONFIG, ...config };
|
|
8929
|
+
}
|
|
8930
|
+
/**
|
|
8931
|
+
* Check if mitmproxy is available
|
|
8932
|
+
*/
|
|
8933
|
+
async checkMitmproxyInstalled() {
|
|
8934
|
+
return new Promise((resolve) => {
|
|
8935
|
+
const proc = spawn2("mitmdump", ["--version"], {
|
|
8936
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
8937
|
+
});
|
|
8938
|
+
proc.on("close", (code) => {
|
|
8939
|
+
resolve(code === 0);
|
|
8940
|
+
});
|
|
8941
|
+
proc.on("error", () => {
|
|
8942
|
+
resolve(false);
|
|
8943
|
+
});
|
|
8944
|
+
});
|
|
8945
|
+
}
|
|
8946
|
+
/**
|
|
8947
|
+
* Ensure the addon script exists at the configured path
|
|
8948
|
+
*/
|
|
8949
|
+
ensureAddonScript() {
|
|
8950
|
+
const addonDir = path3.dirname(this.config.addonPath);
|
|
8951
|
+
if (!fs5.existsSync(addonDir)) {
|
|
8952
|
+
fs5.mkdirSync(addonDir, { recursive: true });
|
|
8953
|
+
}
|
|
8954
|
+
fs5.writeFileSync(this.config.addonPath, RUNNER_MITM_ADDON_SCRIPT, {
|
|
8955
|
+
mode: 493
|
|
8956
|
+
});
|
|
8957
|
+
console.log(
|
|
8958
|
+
`[ProxyManager] Addon script written to ${this.config.addonPath}`
|
|
8959
|
+
);
|
|
8960
|
+
}
|
|
8961
|
+
/**
|
|
8962
|
+
* Validate proxy configuration
|
|
8963
|
+
*/
|
|
8964
|
+
validateConfig() {
|
|
8965
|
+
if (!fs5.existsSync(this.config.caDir)) {
|
|
8966
|
+
throw new Error(`Proxy CA directory not found: ${this.config.caDir}`);
|
|
8967
|
+
}
|
|
8968
|
+
const caCertPath = path3.join(this.config.caDir, "mitmproxy-ca.pem");
|
|
8969
|
+
if (!fs5.existsSync(caCertPath)) {
|
|
8970
|
+
throw new Error(`Proxy CA certificate not found: ${caCertPath}`);
|
|
8971
|
+
}
|
|
8972
|
+
this.ensureAddonScript();
|
|
8973
|
+
}
|
|
8974
|
+
/**
|
|
8975
|
+
* Start mitmproxy
|
|
8976
|
+
*/
|
|
8977
|
+
async start() {
|
|
8978
|
+
if (this.isRunning) {
|
|
8979
|
+
console.log("[ProxyManager] Proxy already running");
|
|
8980
|
+
return;
|
|
8981
|
+
}
|
|
8982
|
+
const mitmproxyInstalled = await this.checkMitmproxyInstalled();
|
|
8983
|
+
if (!mitmproxyInstalled) {
|
|
8984
|
+
throw new Error(
|
|
8985
|
+
"mitmproxy not installed. Install with: pip install mitmproxy"
|
|
8986
|
+
);
|
|
8987
|
+
}
|
|
8988
|
+
this.validateConfig();
|
|
8989
|
+
getVMRegistry();
|
|
8990
|
+
console.log("[ProxyManager] Starting mitmproxy...");
|
|
8991
|
+
console.log(` Port: ${this.config.port}`);
|
|
8992
|
+
console.log(` CA Dir: ${this.config.caDir}`);
|
|
8993
|
+
console.log(` Addon: ${this.config.addonPath}`);
|
|
8994
|
+
console.log(` Registry: ${this.config.registryPath}`);
|
|
8995
|
+
const args = [
|
|
8996
|
+
"--mode",
|
|
8997
|
+
"transparent",
|
|
8998
|
+
"--listen-port",
|
|
8999
|
+
String(this.config.port),
|
|
9000
|
+
"--set",
|
|
9001
|
+
`confdir=${this.config.caDir}`,
|
|
9002
|
+
"--scripts",
|
|
9003
|
+
this.config.addonPath,
|
|
9004
|
+
"--quiet"
|
|
9005
|
+
];
|
|
9006
|
+
const env = {
|
|
9007
|
+
...process.env,
|
|
9008
|
+
VM0_API_URL: this.config.apiUrl,
|
|
9009
|
+
VM0_REGISTRY_PATH: this.config.registryPath
|
|
9010
|
+
};
|
|
9011
|
+
this.process = spawn2("mitmdump", args, {
|
|
9012
|
+
env,
|
|
9013
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9014
|
+
detached: false
|
|
9015
|
+
});
|
|
9016
|
+
this.process.stdout?.on("data", (data) => {
|
|
9017
|
+
console.log(`[mitmproxy] ${data.toString().trim()}`);
|
|
9018
|
+
});
|
|
9019
|
+
this.process.stderr?.on("data", (data) => {
|
|
9020
|
+
console.error(`[mitmproxy] ${data.toString().trim()}`);
|
|
9021
|
+
});
|
|
9022
|
+
this.process.on("close", (code) => {
|
|
9023
|
+
console.log(`[ProxyManager] mitmproxy exited with code ${code}`);
|
|
9024
|
+
this.isRunning = false;
|
|
9025
|
+
this.process = null;
|
|
9026
|
+
});
|
|
9027
|
+
this.process.on("error", (err) => {
|
|
9028
|
+
console.error(`[ProxyManager] mitmproxy error: ${err.message}`);
|
|
9029
|
+
this.isRunning = false;
|
|
9030
|
+
this.process = null;
|
|
9031
|
+
});
|
|
9032
|
+
await this.waitForReady();
|
|
9033
|
+
this.isRunning = true;
|
|
9034
|
+
console.log("[ProxyManager] mitmproxy started successfully");
|
|
9035
|
+
}
|
|
9036
|
+
/**
|
|
9037
|
+
* Wait for proxy to be ready
|
|
9038
|
+
*/
|
|
9039
|
+
async waitForReady(timeoutMs = 1e4) {
|
|
9040
|
+
const startTime = Date.now();
|
|
9041
|
+
const pollInterval = 500;
|
|
9042
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
9043
|
+
if (this.process && this.process.exitCode !== null) {
|
|
9044
|
+
throw new Error(
|
|
9045
|
+
`mitmproxy exited unexpectedly with code ${this.process.exitCode}`
|
|
9046
|
+
);
|
|
9047
|
+
}
|
|
9048
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
9049
|
+
if (this.process && this.process.exitCode === null) {
|
|
9050
|
+
return;
|
|
9051
|
+
}
|
|
9052
|
+
}
|
|
9053
|
+
throw new Error("Timeout waiting for mitmproxy to start");
|
|
9054
|
+
}
|
|
9055
|
+
/**
|
|
9056
|
+
* Stop mitmproxy
|
|
9057
|
+
*/
|
|
9058
|
+
async stop() {
|
|
9059
|
+
if (!this.process || !this.isRunning) {
|
|
9060
|
+
console.log("[ProxyManager] Proxy not running");
|
|
9061
|
+
return;
|
|
9062
|
+
}
|
|
9063
|
+
console.log("[ProxyManager] Stopping mitmproxy...");
|
|
9064
|
+
return new Promise((resolve) => {
|
|
9065
|
+
if (!this.process) {
|
|
9066
|
+
resolve();
|
|
9067
|
+
return;
|
|
9068
|
+
}
|
|
9069
|
+
const timeout = setTimeout(() => {
|
|
9070
|
+
console.log("[ProxyManager] Force killing mitmproxy...");
|
|
9071
|
+
this.process?.kill("SIGKILL");
|
|
9072
|
+
}, 5e3);
|
|
9073
|
+
this.process.on("close", () => {
|
|
9074
|
+
clearTimeout(timeout);
|
|
9075
|
+
this.isRunning = false;
|
|
9076
|
+
this.process = null;
|
|
9077
|
+
console.log("[ProxyManager] mitmproxy stopped");
|
|
9078
|
+
resolve();
|
|
9079
|
+
});
|
|
9080
|
+
this.process.kill("SIGTERM");
|
|
9081
|
+
});
|
|
9082
|
+
}
|
|
9083
|
+
/**
|
|
9084
|
+
* Check if proxy is running
|
|
9085
|
+
*/
|
|
9086
|
+
isProxyRunning() {
|
|
9087
|
+
return this.isRunning && this.process !== null;
|
|
9088
|
+
}
|
|
9089
|
+
/**
|
|
9090
|
+
* Get proxy configuration
|
|
9091
|
+
*/
|
|
9092
|
+
getConfig() {
|
|
9093
|
+
return { ...this.config };
|
|
9094
|
+
}
|
|
9095
|
+
};
|
|
9096
|
+
var globalProxyManager = null;
|
|
9097
|
+
function getProxyManager() {
|
|
9098
|
+
if (!globalProxyManager) {
|
|
9099
|
+
globalProxyManager = new ProxyManager();
|
|
9100
|
+
}
|
|
9101
|
+
return globalProxyManager;
|
|
9102
|
+
}
|
|
9103
|
+
function initProxyManager(config) {
|
|
9104
|
+
globalProxyManager = new ProxyManager(config);
|
|
9105
|
+
return globalProxyManager;
|
|
9106
|
+
}
|
|
9107
|
+
|
|
9108
|
+
// src/lib/executor.ts
|
|
9109
|
+
function getVmIdFromRunId(runId) {
|
|
9110
|
+
return runId.split("-")[0] || runId.substring(0, 8);
|
|
9111
|
+
}
|
|
9112
|
+
function buildEnvironmentVariables(context, apiUrl) {
|
|
9113
|
+
const envVars = {
|
|
9114
|
+
VM0_API_URL: apiUrl,
|
|
9115
|
+
VM0_RUN_ID: context.runId,
|
|
9116
|
+
VM0_API_TOKEN: context.sandboxToken,
|
|
9117
|
+
VM0_PROMPT: context.prompt,
|
|
9118
|
+
VM0_WORKING_DIR: context.workingDir,
|
|
9119
|
+
CLI_AGENT_TYPE: context.cliAgentType || "claude-code"
|
|
9120
|
+
};
|
|
9121
|
+
const vercelBypass = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
9122
|
+
if (vercelBypass) {
|
|
9123
|
+
envVars.VERCEL_PROTECTION_BYPASS = vercelBypass;
|
|
9124
|
+
}
|
|
9125
|
+
const useMockClaude = process.env.USE_MOCK_CLAUDE;
|
|
9126
|
+
if (useMockClaude) {
|
|
9127
|
+
envVars.USE_MOCK_CLAUDE = useMockClaude;
|
|
9128
|
+
}
|
|
9129
|
+
if (context.storageManifest?.artifact) {
|
|
9130
|
+
const artifact = context.storageManifest.artifact;
|
|
9131
|
+
envVars.VM0_ARTIFACT_DRIVER = "vas";
|
|
9132
|
+
envVars.VM0_ARTIFACT_MOUNT_PATH = artifact.mountPath;
|
|
9133
|
+
envVars.VM0_ARTIFACT_VOLUME_NAME = artifact.vasStorageName;
|
|
9134
|
+
envVars.VM0_ARTIFACT_VERSION_ID = artifact.vasVersionId;
|
|
9135
|
+
}
|
|
9136
|
+
if (context.resumeSession) {
|
|
9137
|
+
envVars.VM0_RESUME_SESSION_ID = context.resumeSession.sessionId;
|
|
9138
|
+
}
|
|
9139
|
+
if (context.environment) {
|
|
9140
|
+
Object.assign(envVars, context.environment);
|
|
9141
|
+
}
|
|
9142
|
+
if (context.secretValues && context.secretValues.length > 0) {
|
|
9143
|
+
envVars.VM0_SECRET_VALUES = context.secretValues.map((v) => Buffer.from(v).toString("base64")).join(",");
|
|
8975
9144
|
}
|
|
8976
9145
|
if (context.vars) {
|
|
8977
9146
|
for (const [key, value] of Object.entries(context.vars)) {
|
|
@@ -8981,6 +9150,70 @@ function buildEnvironmentVariables(context, apiUrl) {
|
|
|
8981
9150
|
return envVars;
|
|
8982
9151
|
}
|
|
8983
9152
|
var ENV_JSON_PATH = "/tmp/vm0-env.json";
|
|
9153
|
+
function getNetworkLogPath(runId) {
|
|
9154
|
+
return `/tmp/vm0-network-${runId}.jsonl`;
|
|
9155
|
+
}
|
|
9156
|
+
function readNetworkLogs(runId) {
|
|
9157
|
+
const logPath = getNetworkLogPath(runId);
|
|
9158
|
+
if (!fs6.existsSync(logPath)) {
|
|
9159
|
+
return [];
|
|
9160
|
+
}
|
|
9161
|
+
try {
|
|
9162
|
+
const content = fs6.readFileSync(logPath, "utf-8");
|
|
9163
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
9164
|
+
return lines.map((line) => JSON.parse(line));
|
|
9165
|
+
} catch (err) {
|
|
9166
|
+
console.error(
|
|
9167
|
+
`[Executor] Failed to read network logs: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
9168
|
+
);
|
|
9169
|
+
return [];
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
function cleanupNetworkLogs(runId) {
|
|
9173
|
+
const logPath = getNetworkLogPath(runId);
|
|
9174
|
+
try {
|
|
9175
|
+
if (fs6.existsSync(logPath)) {
|
|
9176
|
+
fs6.unlinkSync(logPath);
|
|
9177
|
+
}
|
|
9178
|
+
} catch (err) {
|
|
9179
|
+
console.error(
|
|
9180
|
+
`[Executor] Failed to cleanup network logs: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
9181
|
+
);
|
|
9182
|
+
}
|
|
9183
|
+
}
|
|
9184
|
+
async function uploadNetworkLogs(apiUrl, sandboxToken, runId) {
|
|
9185
|
+
const networkLogs = readNetworkLogs(runId);
|
|
9186
|
+
if (networkLogs.length === 0) {
|
|
9187
|
+
console.log(`[Executor] No network logs to upload for ${runId}`);
|
|
9188
|
+
return;
|
|
9189
|
+
}
|
|
9190
|
+
console.log(
|
|
9191
|
+
`[Executor] Uploading ${networkLogs.length} network log entries for ${runId}`
|
|
9192
|
+
);
|
|
9193
|
+
const headers = {
|
|
9194
|
+
Authorization: `Bearer ${sandboxToken}`,
|
|
9195
|
+
"Content-Type": "application/json"
|
|
9196
|
+
};
|
|
9197
|
+
const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
9198
|
+
if (bypassSecret) {
|
|
9199
|
+
headers["x-vercel-protection-bypass"] = bypassSecret;
|
|
9200
|
+
}
|
|
9201
|
+
const response = await fetch(`${apiUrl}/api/webhooks/agent/telemetry`, {
|
|
9202
|
+
method: "POST",
|
|
9203
|
+
headers,
|
|
9204
|
+
body: JSON.stringify({
|
|
9205
|
+
runId,
|
|
9206
|
+
networkLogs
|
|
9207
|
+
})
|
|
9208
|
+
});
|
|
9209
|
+
if (!response.ok) {
|
|
9210
|
+
const errorText = await response.text();
|
|
9211
|
+
console.error(`[Executor] Failed to upload network logs: ${errorText}`);
|
|
9212
|
+
return;
|
|
9213
|
+
}
|
|
9214
|
+
console.log(`[Executor] Network logs uploaded successfully for ${runId}`);
|
|
9215
|
+
cleanupNetworkLogs(runId);
|
|
9216
|
+
}
|
|
8984
9217
|
async function uploadScripts(ssh) {
|
|
8985
9218
|
const scripts = getAllScripts();
|
|
8986
9219
|
await ssh.execOrThrow(
|
|
@@ -9030,6 +9263,24 @@ async function restoreSessionHistory(ssh, resumeSession, workingDir, cliAgentTyp
|
|
|
9030
9263
|
`[Executor] Session history restored (${sessionHistory.split("\n").length} lines)`
|
|
9031
9264
|
);
|
|
9032
9265
|
}
|
|
9266
|
+
var PROXY_CA_CERT_PATH = "/opt/vm0-runner/proxy/mitmproxy-ca-cert.pem";
|
|
9267
|
+
async function installProxyCA(ssh) {
|
|
9268
|
+
if (!fs6.existsSync(PROXY_CA_CERT_PATH)) {
|
|
9269
|
+
throw new Error(
|
|
9270
|
+
`Proxy CA certificate not found at ${PROXY_CA_CERT_PATH}. Run generate-proxy-ca.sh first.`
|
|
9271
|
+
);
|
|
9272
|
+
}
|
|
9273
|
+
const caCert = fs6.readFileSync(PROXY_CA_CERT_PATH, "utf-8");
|
|
9274
|
+
console.log(
|
|
9275
|
+
`[Executor] Installing proxy CA certificate (${caCert.length} bytes)`
|
|
9276
|
+
);
|
|
9277
|
+
await ssh.writeFileWithSudo(
|
|
9278
|
+
"/usr/local/share/ca-certificates/vm0-proxy-ca.crt",
|
|
9279
|
+
caCert
|
|
9280
|
+
);
|
|
9281
|
+
await ssh.execOrThrow("sudo update-ca-certificates");
|
|
9282
|
+
console.log(`[Executor] Proxy CA certificate installed successfully`);
|
|
9283
|
+
}
|
|
9033
9284
|
async function configureDNS(ssh) {
|
|
9034
9285
|
const dnsConfig = `nameserver 8.8.8.8
|
|
9035
9286
|
nameserver 8.8.4.4
|
|
@@ -9041,9 +9292,10 @@ nameserver 1.1.1.1`;
|
|
|
9041
9292
|
async function executeJob(context, config) {
|
|
9042
9293
|
const vmId = getVmIdFromRunId(context.runId);
|
|
9043
9294
|
let vm = null;
|
|
9295
|
+
let guestIp = null;
|
|
9044
9296
|
console.log(`[Executor] Starting job ${context.runId} in VM ${vmId}`);
|
|
9045
9297
|
try {
|
|
9046
|
-
const workspacesDir =
|
|
9298
|
+
const workspacesDir = path4.join(process.cwd(), "workspaces");
|
|
9047
9299
|
const vmConfig = {
|
|
9048
9300
|
vmId,
|
|
9049
9301
|
vcpus: config.sandbox.vcpu,
|
|
@@ -9051,12 +9303,12 @@ async function executeJob(context, config) {
|
|
|
9051
9303
|
kernelPath: config.firecracker.kernel,
|
|
9052
9304
|
rootfsPath: config.firecracker.rootfs,
|
|
9053
9305
|
firecrackerBinary: config.firecracker.binary,
|
|
9054
|
-
workDir:
|
|
9306
|
+
workDir: path4.join(workspacesDir, `vm0-${vmId}`)
|
|
9055
9307
|
};
|
|
9056
9308
|
console.log(`[Executor] Creating VM ${vmId}...`);
|
|
9057
9309
|
vm = new FirecrackerVM(vmConfig);
|
|
9058
9310
|
await vm.start();
|
|
9059
|
-
|
|
9311
|
+
guestIp = vm.getGuestIp();
|
|
9060
9312
|
if (!guestIp) {
|
|
9061
9313
|
throw new Error("VM started but no IP address available");
|
|
9062
9314
|
}
|
|
@@ -9066,6 +9318,14 @@ async function executeJob(context, config) {
|
|
|
9066
9318
|
console.log(`[Executor] Waiting for SSH on ${guestIp}...`);
|
|
9067
9319
|
await ssh.waitUntilReachable(12e4, 2e3);
|
|
9068
9320
|
console.log(`[Executor] SSH ready on ${guestIp}`);
|
|
9321
|
+
if (context.experimentalNetworkSecurity) {
|
|
9322
|
+
console.log(
|
|
9323
|
+
`[Executor] Setting up network security mode for VM ${guestIp}`
|
|
9324
|
+
);
|
|
9325
|
+
await setupVMProxyRules(guestIp, config.proxy.port);
|
|
9326
|
+
getVMRegistry().register(guestIp, context.runId, context.sandboxToken);
|
|
9327
|
+
await installProxyCA(ssh);
|
|
9328
|
+
}
|
|
9069
9329
|
console.log(`[Executor] Configuring DNS...`);
|
|
9070
9330
|
await configureDNS(ssh);
|
|
9071
9331
|
console.log(`[Executor] Uploading scripts...`);
|
|
@@ -9138,6 +9398,28 @@ async function executeJob(context, config) {
|
|
|
9138
9398
|
error: errorMsg
|
|
9139
9399
|
};
|
|
9140
9400
|
} finally {
|
|
9401
|
+
if (context.experimentalNetworkSecurity && guestIp) {
|
|
9402
|
+
console.log(`[Executor] Cleaning up network security for VM ${guestIp}`);
|
|
9403
|
+
try {
|
|
9404
|
+
await removeVMProxyRules(guestIp, config.proxy.port);
|
|
9405
|
+
} catch (err) {
|
|
9406
|
+
console.error(
|
|
9407
|
+
`[Executor] Failed to remove VM proxy rules: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
9408
|
+
);
|
|
9409
|
+
}
|
|
9410
|
+
getVMRegistry().unregister(guestIp);
|
|
9411
|
+
try {
|
|
9412
|
+
await uploadNetworkLogs(
|
|
9413
|
+
config.server.url,
|
|
9414
|
+
context.sandboxToken,
|
|
9415
|
+
context.runId
|
|
9416
|
+
);
|
|
9417
|
+
} catch (err) {
|
|
9418
|
+
console.error(
|
|
9419
|
+
`[Executor] Failed to upload network logs: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
9420
|
+
);
|
|
9421
|
+
}
|
|
9422
|
+
}
|
|
9141
9423
|
if (vm) {
|
|
9142
9424
|
console.log(`[Executor] Cleaning up VM ${vmId}...`);
|
|
9143
9425
|
await vm.kill();
|
|
@@ -9197,6 +9479,25 @@ var startCommand = new Command("start").description("Start the runner").option("
|
|
|
9197
9479
|
}
|
|
9198
9480
|
console.log("Setting up network bridge...");
|
|
9199
9481
|
await setupBridge();
|
|
9482
|
+
console.log("Initializing network proxy...");
|
|
9483
|
+
initVMRegistry();
|
|
9484
|
+
const proxyManager = initProxyManager({
|
|
9485
|
+
apiUrl: config.server.url,
|
|
9486
|
+
port: config.proxy.port
|
|
9487
|
+
});
|
|
9488
|
+
let proxyEnabled = false;
|
|
9489
|
+
try {
|
|
9490
|
+
await proxyManager.start();
|
|
9491
|
+
proxyEnabled = true;
|
|
9492
|
+
console.log("Network proxy initialized successfully");
|
|
9493
|
+
} catch (err) {
|
|
9494
|
+
console.warn(
|
|
9495
|
+
`Network proxy not available: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
9496
|
+
);
|
|
9497
|
+
console.warn(
|
|
9498
|
+
"Jobs with experimentalNetworkSecurity enabled will run without network interception"
|
|
9499
|
+
);
|
|
9500
|
+
}
|
|
9200
9501
|
const statusFilePath = join(dirname(options.config), "status.json");
|
|
9201
9502
|
const startedAt = /* @__PURE__ */ new Date();
|
|
9202
9503
|
const state = { mode: "running" };
|
|
@@ -9300,6 +9601,10 @@ var startCommand = new Command("start").description("Start the runner").option("
|
|
|
9300
9601
|
);
|
|
9301
9602
|
await Promise.all(jobPromises);
|
|
9302
9603
|
}
|
|
9604
|
+
if (proxyEnabled) {
|
|
9605
|
+
console.log("Stopping network proxy...");
|
|
9606
|
+
await getProxyManager().stop();
|
|
9607
|
+
}
|
|
9303
9608
|
state.mode = "stopped";
|
|
9304
9609
|
updateStatus();
|
|
9305
9610
|
console.log("Runner stopped");
|
|
@@ -9339,7 +9644,7 @@ var statusCommand = new Command2("status").description("Check runner connectivit
|
|
|
9339
9644
|
});
|
|
9340
9645
|
|
|
9341
9646
|
// src/index.ts
|
|
9342
|
-
var version = true ? "2.
|
|
9647
|
+
var version = true ? "2.3.0" : "0.1.0";
|
|
9343
9648
|
program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
|
|
9344
9649
|
program.addCommand(startCommand);
|
|
9345
9650
|
program.addCommand(statusCommand);
|