@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.
Files changed (2) hide show
  1. package/index.js +765 -460
  2. 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 path3 from "path";
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, path4, body) {
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} ${path4}${bodyStr ? ` (${Buffer.byteLength(bodyStr)} bytes)` : ""}`
173
+ `[FC API] ${method} ${path5}${bodyStr ? ` (${Buffer.byteLength(bodyStr)} bytes)` : ""}`
170
174
  );
171
175
  const options = {
172
176
  socketPath: this.socketPath,
173
- path: path4,
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: path4, errorMaps, issueData } = params;
1282
- const fullPath = [...path4, ...issueData.path || []];
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, path4, key) {
1431
+ constructor(parent, value, path5, key) {
1382
1432
  this._cachedPath = [];
1383
1433
  this.parent = parent;
1384
1434
  this.data = value;
1385
- this._path = path4;
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/executor.ts
8940
- function getVmIdFromRunId(runId) {
8941
- return runId.split("-")[0] || runId.substring(0, 8);
8942
- }
8943
- function buildEnvironmentVariables(context, apiUrl) {
8944
- const envVars = {
8945
- VM0_API_URL: apiUrl,
8946
- VM0_RUN_ID: context.runId,
8947
- VM0_API_TOKEN: context.sandboxToken,
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
- if (context.storageManifest?.artifact) {
8961
- const artifact = context.storageManifest.artifact;
8962
- envVars.VM0_ARTIFACT_DRIVER = "vas";
8963
- envVars.VM0_ARTIFACT_MOUNT_PATH = artifact.mountPath;
8964
- envVars.VM0_ARTIFACT_VOLUME_NAME = artifact.vasStorageName;
8965
- envVars.VM0_ARTIFACT_VERSION_ID = artifact.vasVersionId;
8966
- }
8967
- if (context.resumeSession) {
8968
- envVars.VM0_RESUME_SESSION_ID = context.resumeSession.sessionId;
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
- if (context.environment) {
8971
- Object.assign(envVars, context.environment);
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
- if (context.secretValues && context.secretValues.length > 0) {
8974
- envVars.VM0_SECRET_VALUES = context.secretValues.map((v) => Buffer.from(v).toString("base64")).join(",");
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 = path3.join(process.cwd(), "workspaces");
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: path3.join(workspacesDir, `vm0-${vmId}`)
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
- const guestIp = vm.getGuestIp();
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.2.2" : "0.1.0";
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vm0/runner",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "Self-hosted runner for VM0 agents",
5
5
  "repository": {
6
6
  "type": "git",