@hotcell/daemon 0.1.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 (142) hide show
  1. package/dist/agent-check.d.ts +1 -0
  2. package/dist/agent-check.js +105 -0
  3. package/dist/agent-check.js.map +1 -0
  4. package/dist/api/server.d.ts +64 -0
  5. package/dist/api/server.js +1368 -0
  6. package/dist/api/server.js.map +1 -0
  7. package/dist/api/ws.d.ts +14 -0
  8. package/dist/api/ws.js +214 -0
  9. package/dist/api/ws.js.map +1 -0
  10. package/dist/applevz-check.d.ts +1 -0
  11. package/dist/applevz-check.js +280 -0
  12. package/dist/applevz-check.js.map +1 -0
  13. package/dist/backups.d.ts +29 -0
  14. package/dist/backups.js +76 -0
  15. package/dist/backups.js.map +1 -0
  16. package/dist/capacity.d.ts +63 -0
  17. package/dist/capacity.js +95 -0
  18. package/dist/capacity.js.map +1 -0
  19. package/dist/config.d.ts +241 -0
  20. package/dist/config.js +152 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/cost.d.ts +9 -0
  23. package/dist/cost.js +19 -0
  24. package/dist/cost.js.map +1 -0
  25. package/dist/density-bench.d.ts +1 -0
  26. package/dist/density-bench.js +97 -0
  27. package/dist/density-bench.js.map +1 -0
  28. package/dist/driver/agent-driver.d.ts +67 -0
  29. package/dist/driver/agent-driver.js +197 -0
  30. package/dist/driver/agent-driver.js.map +1 -0
  31. package/dist/driver/agent.d.ts +94 -0
  32. package/dist/driver/agent.js +319 -0
  33. package/dist/driver/agent.js.map +1 -0
  34. package/dist/driver/applevz.d.ts +95 -0
  35. package/dist/driver/applevz.js +527 -0
  36. package/dist/driver/applevz.js.map +1 -0
  37. package/dist/driver/container.d.ts +99 -0
  38. package/dist/driver/container.js +733 -0
  39. package/dist/driver/container.js.map +1 -0
  40. package/dist/driver/fc-api.d.ts +85 -0
  41. package/dist/driver/fc-api.js +173 -0
  42. package/dist/driver/fc-api.js.map +1 -0
  43. package/dist/driver/firecracker.d.ts +126 -0
  44. package/dist/driver/firecracker.js +560 -0
  45. package/dist/driver/firecracker.js.map +1 -0
  46. package/dist/driver/index.d.ts +17 -0
  47. package/dist/driver/index.js +61 -0
  48. package/dist/driver/index.js.map +1 -0
  49. package/dist/driver/router.d.ts +82 -0
  50. package/dist/driver/router.js +159 -0
  51. package/dist/driver/router.js.map +1 -0
  52. package/dist/driver/types.d.ts +234 -0
  53. package/dist/driver/types.js +2 -0
  54. package/dist/driver/types.js.map +1 -0
  55. package/dist/driver/unsupported.d.ts +50 -0
  56. package/dist/driver/unsupported.js +81 -0
  57. package/dist/driver/unsupported.js.map +1 -0
  58. package/dist/driver/vz-image.d.ts +35 -0
  59. package/dist/driver/vz-image.js +75 -0
  60. package/dist/driver/vz-image.js.map +1 -0
  61. package/dist/e2e-harness.d.ts +1 -0
  62. package/dist/e2e-harness.js +99 -0
  63. package/dist/e2e-harness.js.map +1 -0
  64. package/dist/egress-check.d.ts +8 -0
  65. package/dist/egress-check.js +317 -0
  66. package/dist/egress-check.js.map +1 -0
  67. package/dist/fc-check.d.ts +1 -0
  68. package/dist/fc-check.js +113 -0
  69. package/dist/fc-check.js.map +1 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +114 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/kernels.d.ts +22 -0
  74. package/dist/kernels.js +134 -0
  75. package/dist/kernels.js.map +1 -0
  76. package/dist/lifecycle.d.ts +49 -0
  77. package/dist/lifecycle.js +111 -0
  78. package/dist/lifecycle.js.map +1 -0
  79. package/dist/logger.d.ts +16 -0
  80. package/dist/logger.js +67 -0
  81. package/dist/logger.js.map +1 -0
  82. package/dist/metrics.d.ts +46 -0
  83. package/dist/metrics.js +92 -0
  84. package/dist/metrics.js.map +1 -0
  85. package/dist/net/firewall.d.ts +4 -0
  86. package/dist/net/firewall.js +80 -0
  87. package/dist/net/firewall.js.map +1 -0
  88. package/dist/pricing.d.ts +36 -0
  89. package/dist/pricing.js +81 -0
  90. package/dist/pricing.js.map +1 -0
  91. package/dist/proxy/allowlist-default.d.ts +23 -0
  92. package/dist/proxy/allowlist-default.js +53 -0
  93. package/dist/proxy/allowlist-default.js.map +1 -0
  94. package/dist/proxy/allowlist.d.ts +43 -0
  95. package/dist/proxy/allowlist.js +87 -0
  96. package/dist/proxy/allowlist.js.map +1 -0
  97. package/dist/proxy/egress.d.ts +49 -0
  98. package/dist/proxy/egress.js +599 -0
  99. package/dist/proxy/egress.js.map +1 -0
  100. package/dist/proxy/ratelimit.d.ts +49 -0
  101. package/dist/proxy/ratelimit.js +74 -0
  102. package/dist/proxy/ratelimit.js.map +1 -0
  103. package/dist/proxy/server.d.ts +26 -0
  104. package/dist/proxy/server.js +202 -0
  105. package/dist/proxy/server.js.map +1 -0
  106. package/dist/router-check.d.ts +1 -0
  107. package/dist/router-check.js +97 -0
  108. package/dist/router-check.js.map +1 -0
  109. package/dist/smoke-fc.d.ts +1 -0
  110. package/dist/smoke-fc.js +183 -0
  111. package/dist/smoke-fc.js.map +1 -0
  112. package/dist/smoke-microvm.d.ts +1 -0
  113. package/dist/smoke-microvm.js +187 -0
  114. package/dist/smoke-microvm.js.map +1 -0
  115. package/dist/smoke-remote.d.ts +20 -0
  116. package/dist/smoke-remote.js +155 -0
  117. package/dist/smoke-remote.js.map +1 -0
  118. package/dist/smoke-vz.d.ts +1 -0
  119. package/dist/smoke-vz.js +161 -0
  120. package/dist/smoke-vz.js.map +1 -0
  121. package/dist/smoke.d.ts +7 -0
  122. package/dist/smoke.js +962 -0
  123. package/dist/smoke.js.map +1 -0
  124. package/dist/store.d.ts +124 -0
  125. package/dist/store.js +578 -0
  126. package/dist/store.js.map +1 -0
  127. package/dist/tracing.d.ts +45 -0
  128. package/dist/tracing.js +144 -0
  129. package/dist/tracing.js.map +1 -0
  130. package/dist/types.d.ts +274 -0
  131. package/dist/types.js +3 -0
  132. package/dist/types.js.map +1 -0
  133. package/dist/util.d.ts +27 -0
  134. package/dist/util.js +63 -0
  135. package/dist/util.js.map +1 -0
  136. package/dist/warmpool-check.d.ts +1 -0
  137. package/dist/warmpool-check.js +83 -0
  138. package/dist/warmpool-check.js.map +1 -0
  139. package/dist/web/dashboard.d.ts +1 -0
  140. package/dist/web/dashboard.js +456 -0
  141. package/dist/web/dashboard.js.map +1 -0
  142. package/package.json +53 -0
@@ -0,0 +1,241 @@
1
+ export interface Config {
2
+ host: string;
3
+ port: number;
4
+ /**
5
+ * Default OCI image for new sandboxes. Defaults to a small Debian-based
6
+ * Python image (ships /bin/bash + python3) so the vertical slice works with
7
+ * zero local image builds. Build images/base and set SBX_IMAGE=sbx/base:latest
8
+ * for the richer toolset (Node 20, git, ripgrep, …).
9
+ */
10
+ defaultImage: string;
11
+ /**
12
+ * Runtime driver to use: `container` (Docker, the only built impl),
13
+ * `firecracker` (Linux microVM, Phase 3), or `applevz` (macOS microVM, Phase 3).
14
+ * Selects the isolation tier; the daemon/SDK/CLI are unchanged across drivers.
15
+ */
16
+ driver: string;
17
+ /** Default per-sandbox memory cap in MiB for new sandboxes (`0` = unlimited). */
18
+ defaultMemoryMb: number;
19
+ /** Default per-sandbox CPU cap in fractional cores (`0` = unlimited). */
20
+ defaultCpus: number;
21
+ /** Default per-sandbox process/thread cap for new sandboxes (`0` = unlimited). */
22
+ defaultPidsLimit: number;
23
+ /**
24
+ * Admission control: `enforce` rejects `create` when the host's memory budget
25
+ * is exhausted (prevents over-subscription / OOM when launching many
26
+ * sandboxes); `off` only reports capacity. Default `enforce`.
27
+ */
28
+ admission: "enforce" | "off";
29
+ /** Host memory budget in MiB for admission (`0` = auto-detect from the runtime). */
30
+ hostMemoryMb: number;
31
+ /** Host CPU budget for the capacity view (`0` = auto-detect from the runtime). */
32
+ hostCpus: number;
33
+ /** Memory over-commit factor applied to the host budget (e.g. `1.5` to oversubscribe). */
34
+ overcommit: number;
35
+ /**
36
+ * Admission **floor** (MiB) charged for an uncapped sandbox that's just started
37
+ * or not yet sampled. Once the metrics sampler sees it, admission uses its
38
+ * measured RSS instead (usage-based). Capped sandboxes use their cap.
39
+ */
40
+ defaultReservationMb: number;
41
+ /** Bind host for the preview-URL reverse proxy. */
42
+ proxyHost: string;
43
+ /** Port for the preview-URL reverse proxy (separate from the REST API). */
44
+ proxyPort: number;
45
+ /**
46
+ * Bind host for the egress credential proxy (LLM gateway). Defaults to
47
+ * loopback (single-tenant safe). On **native Linux** dockerd, sandboxes reach
48
+ * the daemon via the bridge gateway (`host.docker.internal` → ~172.17.0.1), so
49
+ * a loopback-only bind is unreachable from sandboxes — set `HOTCELL_EGRESS_HOST`
50
+ * to `0.0.0.0` (or the bridge IP) there, relying on the per-sandbox egress
51
+ * token + host firewall. Docker Desktop (macOS) reaches loopback as-is.
52
+ */
53
+ egressHost: string;
54
+ /** Port for the egress credential proxy. `0` disables it. */
55
+ egressPort: number;
56
+ /**
57
+ * Enforce default-deny egress: put sandboxes on a dedicated bridge network and
58
+ * (Linux only) install host `DOCKER-USER` iptables rules so the ONLY reachable
59
+ * destinations are the egress gateway + the pinned DNS resolver — everything else
60
+ * is dropped. Off by default (the gateway is then the "safe path available", not
61
+ * the "only path"). `HOTCELL_EGRESS_ENFORCE`. Advisory-only on macOS Docker Desktop.
62
+ */
63
+ egressEnforce: boolean;
64
+ /** Docker network name sandboxes join when egress is enforced. `HOTCELL_EGRESS_NETWORK`. */
65
+ egressNetwork: string;
66
+ /**
67
+ * IPv4 subnet (CIDR) for the egress-enforced bridge network. The host firewall
68
+ * scopes its allow/deny rules to this subnet, and the bridge gateway (`.1`) is the
69
+ * sandbox's route to the egress gateway. `/24` assumed. `HOTCELL_EGRESS_SUBNET`.
70
+ */
71
+ egressSubnet: string;
72
+ /**
73
+ * DNS resolver IP pinned into sandboxes under egress enforcement (so DNS can't be
74
+ * used as an exfil channel and DoH is the only blocked path). Empty = Docker's
75
+ * embedded resolver. `HOTCELL_EGRESS_DNS`.
76
+ */
77
+ egressDnsResolver: string;
78
+ /**
79
+ * Path to a JSON `{ allow, deny }` allowlist that fully REPLACES the built-in
80
+ * default (forward-proxy host allowlist). `HOTCELL_ALLOWLIST_FILE`. Empty = defaults.
81
+ */
82
+ allowlistFile: string;
83
+ /** Extra hosts appended to the allowlist (`HOTCELL_ALLOWLIST_EXTRA`, comma-separated). */
84
+ allowlistExtra: string[];
85
+ /**
86
+ * Include the `source_control` allowlist tier (github/gitlab/bitbucket). On by
87
+ * default so `git clone` works; set `HOTCELL_ALLOW_SOURCE_CONTROL=false` for
88
+ * high-security workloads (a writable git host is also an exfil channel).
89
+ */
90
+ allowSourceControl: boolean;
91
+ /**
92
+ * Default per-sandbox LLM spend ceiling in USD — the gateway returns 402 once a
93
+ * sandbox's cumulative provider cost reaches it, across all its tokens. Per-create
94
+ * `egressSpendCapUsd` overrides. `0` = unlimited. `HOTCELL_EGRESS_SPEND_CAP`.
95
+ */
96
+ egressSpendCapUsd: number;
97
+ /**
98
+ * Hostname advertised in egress base URLs returned to clients. Sandboxes reach
99
+ * the daemon host through this name, so on Docker Desktop it defaults to
100
+ * `host.docker.internal`. Set to a LAN IP/DNS name for remote sandboxes.
101
+ */
102
+ egressAdvertiseHost: string;
103
+ /**
104
+ * Provider API keys, held on the daemon host and injected into outbound calls
105
+ * so they never live inside a sandbox. Keyed by lower-case provider name
106
+ * (`openai`, `anthropic`, `openrouter`). A provider's gateway route exists only
107
+ * when its key is present here. Sourced from `HOTCELL_PROVIDER_KEY_<NAME>` env vars.
108
+ */
109
+ providerKeys: Record<string, string>;
110
+ /**
111
+ * Operator-defined / override provider shapes, keyed by lower-case provider
112
+ * name. Lets an operator add a provider the daemon doesn't ship (or repoint a
113
+ * built-in — e.g. route `openai` through a Cloudflare AI Gateway prefix) without
114
+ * a code change. Sourced from `HOTCELL_PROVIDER_<NAME>_BASEURL` / `_AUTHHEADER` /
115
+ * `_FORMAT`. A route still requires a key (`HOTCELL_PROVIDER_KEY_<NAME>`).
116
+ */
117
+ providerConfigs: Record<string, ProviderConfig>;
118
+ /** Host directory where sandbox backup tarballs + metadata are stored. */
119
+ backupDir: string;
120
+ /**
121
+ * Path to the embedded SQLite database holding durable control-plane state
122
+ * (sandbox/process/session/context/exposed-port records). Survives daemon
123
+ * restarts. Set to `:memory:` for an ephemeral, in-process store.
124
+ */
125
+ dbPath: string;
126
+ /**
127
+ * Default idle timeout (ms) applied to new sandboxes that don't specify their
128
+ * own `sleepAfter`. The reaper auto-pauses a sandbox after this long without
129
+ * activity. `0` (the default) disables auto-pause.
130
+ */
131
+ defaultSleepAfterMs: number;
132
+ /** How often (ms) the idle reaper scans for sandboxes to auto-pause. */
133
+ reapIntervalMs: number;
134
+ /** How often (ms) the metrics sampler integrates per-sandbox usage. `0` disables it. */
135
+ metricsIntervalMs: number;
136
+ /** Cost-meter rate: currency units per vCPU-hour. */
137
+ costCpuPerHour: number;
138
+ /** Cost-meter rate: currency units per GB-hour of memory. */
139
+ costMemGbPerHour: number;
140
+ /** Cost-meter rate: currency units per GB of preview-proxy egress. */
141
+ costEgressPerGb: number;
142
+ /**
143
+ * Path to a JSON model→price override file (`HOTCELL_MODEL_PRICES`), overlaid on the
144
+ * built-in table. Used to compute an LLM call's USD cost when the provider does
145
+ * not report one. Empty = built-in defaults only.
146
+ */
147
+ modelPricesPath: string;
148
+ /**
149
+ * Path to the signed `sbx-vz` Swift helper for the Apple VZ driver (`HOTCELL_DRIVER=applevz`).
150
+ * Built by `npm run build:vz`. Defaults to its in-repo build output; set
151
+ * `HOTCELL_VZ_HELPER_PATH` to a bundled/installed location. `HOTCELL_DRIVER=container` ignores it.
152
+ */
153
+ vzHelperPath: string;
154
+ /** Guest kernel image for the Apple VZ driver (`HOTCELL_VZ_KERNEL`). Uncompressed arm64 Image. */
155
+ vzKernel: string;
156
+ /** Base rootfs image for the Apple VZ driver (`HOTCELL_VZ_ROOTFS`). */
157
+ vzRootfs: string;
158
+ /** Per-sandbox VM state dir (disks, sockets) for the Apple VZ driver (`HOTCELL_VZ_STATE_DIR`). */
159
+ vzStateDir: string;
160
+ /** Default workspace disk size in GiB for a new VZ sandbox (`HOTCELL_VZ_DISK_GB`). */
161
+ vzDiskGb: number;
162
+ /**
163
+ * Cache dir for OCI→ext4 converted rootfs images + the blank workspace template
164
+ * (`HOTCELL_VZ_IMAGE_CACHE`). Keyed by image name; populated on demand from
165
+ * `HOTCELL_IMAGE`. Kept separate from the per-sandbox state dir so it survives.
166
+ */
167
+ vzImageCacheDir: string;
168
+ /**
169
+ * Warm-pool size for the Apple VZ driver (`HOTCELL_VZ_WARM_POOL`, default 0 = off):
170
+ * keep this many base-image microVMs pre-booted so a `create` of the base image
171
+ * is an instant adopt instead of a ~2s cold boot. The pool refills in the
172
+ * background as guests are claimed.
173
+ */
174
+ vzWarmPool: number;
175
+ /** `firecracker` binary path for the Firecracker driver (`HOTCELL_FC_BIN`). */
176
+ fcBin: string;
177
+ /** Guest kernel (uncompressed vmlinux) for the Firecracker driver (`HOTCELL_FC_KERNEL`). */
178
+ fcKernel: string;
179
+ /** Prebuilt base rootfs for the Firecracker driver (`HOTCELL_FC_ROOTFS`). */
180
+ fcRootfs: string;
181
+ /** Per-sandbox VM state dir for the Firecracker driver (`HOTCELL_FC_STATE_DIR`). */
182
+ fcStateDir: string;
183
+ /** Default workspace disk size in GiB for a new Firecracker sandbox (`HOTCELL_FC_DISK_GB`). */
184
+ fcDiskGb: number;
185
+ /**
186
+ * Warm-pool size for the Firecracker driver (`HOTCELL_FC_WARM_POOL`, default 0 = off):
187
+ * keep N pre-booted spare microVMs of the default image so a plain create
188
+ * adopts one instantly instead of cold-booting.
189
+ */
190
+ fcWarmPool: number;
191
+ /** Converted OCI→ext4 rootfs cache for the Firecracker driver (`HOTCELL_FC_IMAGE_CACHE`). */
192
+ fcImageCacheDir: string;
193
+ /** Minimum level emitted by the structured logger. */
194
+ logLevel: LogLevel;
195
+ /** Log encoding: `json` (one JSON object per line) or `pretty` (human). */
196
+ logFormat: LogFormat;
197
+ /**
198
+ * API key required on every REST call (except `/healthz` and the dashboard
199
+ * HTML). Empty string (the default) disables auth — loopback, single-tenant.
200
+ */
201
+ apiKey: string;
202
+ /**
203
+ * OTLP/HTTP traces endpoint (e.g. `http://localhost:4318`). When set, spans are
204
+ * batch-exported to `<endpoint>/v1/traces`. Empty disables export; recent spans
205
+ * are always kept in-memory for `GET /traces` regardless.
206
+ */
207
+ otlpEndpoint: string;
208
+ /** How many of the most recent metric samples per sandbox to retain in memory. */
209
+ metricsHistory: number;
210
+ /** How many of the most recent finished spans to retain in memory for `/traces`. */
211
+ traceRing: number;
212
+ /**
213
+ * Maximum accepted request body size in bytes (REST + egress gateway). Caps
214
+ * memory use from a single request. Must stay large enough for base64-encoded
215
+ * file writes. `HOTCELL_MAX_BODY_BYTES`, default 32 MiB.
216
+ */
217
+ maxBodyBytes: number;
218
+ /**
219
+ * Extra Host header values accepted by the API server, beyond the loopback
220
+ * names and `host`. The Host allowlist is a DNS-rebinding / localhost-CSRF
221
+ * guard. `HOTCELL_ALLOWED_HOSTS` (comma-separated). Empty by default.
222
+ */
223
+ allowedHosts: string[];
224
+ }
225
+ export type LogLevel = "debug" | "info" | "warn" | "error";
226
+ export type LogFormat = "json" | "pretty";
227
+ /** Operator-supplied provider shape (base URL + auth header + key-format template). */
228
+ export interface ProviderConfig {
229
+ baseUrl?: string;
230
+ /** Header the provider authenticates with (e.g. `authorization`, `x-api-key`). */
231
+ authHeader?: string;
232
+ /** Auth-header value template; `{key}` is replaced with the real key (e.g. `Bearer {key}`). */
233
+ formatTemplate?: string;
234
+ }
235
+ export declare function loadConfig(): Config;
236
+ /**
237
+ * Build the preview URL for an exposed port. Uses a `*.localhost` subdomain,
238
+ * which browsers resolve to 127.0.0.1 with zero DNS/hosts config. Sandbox ids
239
+ * are dash-free hex, so `<id>-<port>` is an unambiguous label.
240
+ */
241
+ export declare function previewUrl(config: Config, sandboxId: string, port: number): string;
package/dist/config.js ADDED
@@ -0,0 +1,152 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ /**
5
+ * Read a daemon env var with legacy fallback: `HOTCELL_<name>` wins, then the
6
+ * pre-rename `HOTCELL_<name>` — so deployments configured before the hotcell
7
+ * rename keep working unchanged.
8
+ */
9
+ function env(name) {
10
+ return process.env[`HOTCELL_${name}`] ?? process.env[`HOTCELL_${name}`];
11
+ }
12
+ /**
13
+ * Root for daemon state (db, backups, VM state, image caches): `~/.hotcell`,
14
+ * except when a pre-rename `~/.sbx` already exists and `~/.hotcell` doesn't —
15
+ * then the legacy root keeps being used so existing installs keep their data.
16
+ */
17
+ function stateRoot() {
18
+ const modern = join(homedir(), ".hotcell");
19
+ const legacy = join(homedir(), ".sbx");
20
+ if (!existsSync(modern) && existsSync(legacy))
21
+ return legacy;
22
+ return modern;
23
+ }
24
+ export function loadConfig() {
25
+ return {
26
+ host: env("HOST") ?? "127.0.0.1",
27
+ port: Number(env("PORT") ?? 4750),
28
+ defaultImage: env("IMAGE") ?? "python:3.11-slim-bookworm",
29
+ driver: env("DRIVER") ?? "container",
30
+ defaultMemoryMb: Number(env("DEFAULT_MEMORY_MB") ?? 0),
31
+ defaultCpus: Number(env("DEFAULT_CPUS") ?? 0),
32
+ defaultPidsLimit: Number(env("DEFAULT_PIDS") ?? 0),
33
+ admission: env("ADMISSION") === "off" ? "off" : "enforce",
34
+ hostMemoryMb: Number(env("HOST_MEMORY_MB") ?? 0),
35
+ hostCpus: Number(env("HOST_CPUS") ?? 0),
36
+ overcommit: Number(env("OVERCOMMIT") ?? 1),
37
+ defaultReservationMb: Number(env("DEFAULT_RESERVATION_MB") ?? 256),
38
+ proxyHost: env("PROXY_HOST") ?? "127.0.0.1",
39
+ proxyPort: Number(env("PROXY_PORT") ?? 4751),
40
+ egressHost: env("EGRESS_HOST") ?? "127.0.0.1",
41
+ egressPort: Number(env("EGRESS_PORT") ?? 4752),
42
+ egressAdvertiseHost: env("EGRESS_ADVERTISE_HOST") ?? "host.docker.internal",
43
+ egressEnforce: env("EGRESS_ENFORCE") === "true",
44
+ egressNetwork: env("EGRESS_NETWORK") ?? "hotcell-egress",
45
+ egressSubnet: env("EGRESS_SUBNET") ?? "10.200.0.0/24",
46
+ egressDnsResolver: env("EGRESS_DNS") ?? "",
47
+ allowlistFile: env("ALLOWLIST_FILE") ?? "",
48
+ allowlistExtra: (env("ALLOWLIST_EXTRA") ?? "")
49
+ .split(",")
50
+ .map((h) => h.trim())
51
+ .filter(Boolean),
52
+ allowSourceControl: env("ALLOW_SOURCE_CONTROL") !== "false",
53
+ egressSpendCapUsd: Number(env("EGRESS_SPEND_CAP") ?? 0),
54
+ providerKeys: loadProviderKeys(),
55
+ providerConfigs: loadProviderConfigs(),
56
+ backupDir: env("BACKUP_DIR") ?? join(stateRoot(), "backups"),
57
+ dbPath: env("DB") ?? join(stateRoot(), "state.db"),
58
+ defaultSleepAfterMs: Number(env("SLEEP_AFTER_MS") ?? 0),
59
+ reapIntervalMs: Number(env("REAP_INTERVAL_MS") ?? 15000),
60
+ metricsIntervalMs: Number(env("METRICS_INTERVAL_MS") ?? 10000),
61
+ costCpuPerHour: Number(env("COST_CPU_PER_HOUR") ?? 0.05),
62
+ costMemGbPerHour: Number(env("COST_MEM_GB_PER_HOUR") ?? 0.005),
63
+ costEgressPerGb: Number(env("COST_EGRESS_PER_GB") ?? 0.01),
64
+ modelPricesPath: env("MODEL_PRICES") ?? "",
65
+ vzHelperPath: env("VZ_HELPER_PATH") ?? "helpers/hotcell-vz/dist/hotcell-vz",
66
+ vzKernel: env("VZ_KERNEL") ?? "helpers/hotcell-vz/guest/vmlinux-vz",
67
+ vzRootfs: env("VZ_ROOTFS") ?? "helpers/hotcell-vz/guest/rootfs.img",
68
+ vzStateDir: env("VZ_STATE_DIR") ?? join(stateRoot(), "vz"),
69
+ vzDiskGb: Number(env("VZ_DISK_GB") ?? 4),
70
+ vzImageCacheDir: env("VZ_IMAGE_CACHE") ?? join(stateRoot(), "vz", "images"),
71
+ vzWarmPool: Number(env("VZ_WARM_POOL") ?? 0),
72
+ fcBin: env("FC_BIN") ?? "firecracker",
73
+ fcKernel: env("FC_KERNEL") ?? "helpers/hotcell-vz/guest/vmlinux-fc",
74
+ fcRootfs: env("FC_ROOTFS") ?? "helpers/hotcell-vz/guest/rootfs.img",
75
+ fcStateDir: env("FC_STATE_DIR") ?? join(stateRoot(), "fc"),
76
+ fcDiskGb: Number(env("FC_DISK_GB") ?? 8),
77
+ fcWarmPool: Number(env("FC_WARM_POOL") ?? 0),
78
+ fcImageCacheDir: env("FC_IMAGE_CACHE") ?? join(stateRoot(), "fc", "images"),
79
+ logLevel: parseLogLevel(env("LOG_LEVEL")),
80
+ logFormat: env("LOG_FORMAT") === "json" ? "json" : "pretty",
81
+ apiKey: env("API_KEY") ?? "",
82
+ otlpEndpoint: (env("OTLP_ENDPOINT") ?? "").replace(/\/$/, ""),
83
+ metricsHistory: Number(env("METRICS_HISTORY") ?? 60),
84
+ traceRing: Number(env("TRACE_RING") ?? 200),
85
+ maxBodyBytes: Number(env("MAX_BODY_BYTES") ?? 32 * 1024 * 1024),
86
+ allowedHosts: (env("ALLOWED_HOSTS") ?? "")
87
+ .split(",")
88
+ .map((h) => h.trim())
89
+ .filter(Boolean),
90
+ };
91
+ }
92
+ /** Collect provider keys from `HOTCELL_PROVIDER_KEY_<NAME>` env vars (name
93
+ * lowercased); legacy `SBX_PROVIDER_KEY_<NAME>` still read, HOTCELL_ wins. */
94
+ function loadProviderKeys() {
95
+ const keys = {};
96
+ for (const prefix of ["SBX_PROVIDER_KEY_", "HOTCELL_PROVIDER_KEY_"]) {
97
+ // legacy first so HOTCELL_ overwrites on conflict
98
+ for (const [name, value] of Object.entries(process.env)) {
99
+ if (name.startsWith(prefix) && value) {
100
+ keys[name.slice(prefix.length).toLowerCase()] = value;
101
+ }
102
+ }
103
+ }
104
+ return keys;
105
+ }
106
+ /**
107
+ * Collect operator-defined provider shapes from `HOTCELL_PROVIDER_<NAME>_<FIELD>` env
108
+ * vars (FIELD ∈ BASEURL/AUTHHEADER/FORMAT), skipping the `HOTCELL_PROVIDER_KEY_*`
109
+ * namespace. The provider name is lower-cased; e.g.
110
+ * `HOTCELL_PROVIDER_CFOPENAI_BASEURL=https://gateway.ai.cloudflare.com/v1/<acct>/<gw>/openai`
111
+ * defines a `cfopenai` route (pair with `HOTCELL_PROVIDER_KEY_CFOPENAI`).
112
+ */
113
+ function loadProviderConfigs() {
114
+ const out = {};
115
+ // Legacy prefix scanned first so HOTCELL_ overwrites on conflict.
116
+ for (const prefix of ["SBX_PROVIDER_", "HOTCELL_PROVIDER_"]) {
117
+ for (const [name, value] of Object.entries(process.env)) {
118
+ if (!name.startsWith(prefix) || !value)
119
+ continue;
120
+ if (name.startsWith("SBX_PROVIDER_KEY_") || name.startsWith("HOTCELL_PROVIDER_KEY_"))
121
+ continue;
122
+ const rest = name.slice(prefix.length); // <NAME>_<FIELD>
123
+ const us = rest.lastIndexOf("_");
124
+ if (us <= 0)
125
+ continue;
126
+ const provName = rest.slice(0, us).toLowerCase();
127
+ const field = rest.slice(us + 1).toUpperCase();
128
+ const cfg = (out[provName] ??= {});
129
+ if (field === "BASEURL")
130
+ cfg.baseUrl = value;
131
+ else if (field === "AUTHHEADER")
132
+ cfg.authHeader = value.toLowerCase();
133
+ else if (field === "FORMAT")
134
+ cfg.formatTemplate = value;
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ function parseLogLevel(value) {
140
+ return value === "debug" || value === "info" || value === "warn" || value === "error"
141
+ ? value
142
+ : "info";
143
+ }
144
+ /**
145
+ * Build the preview URL for an exposed port. Uses a `*.localhost` subdomain,
146
+ * which browsers resolve to 127.0.0.1 with zero DNS/hosts config. Sandbox ids
147
+ * are dash-free hex, so `<id>-<port>` is an unambiguous label.
148
+ */
149
+ export function previewUrl(config, sandboxId, port) {
150
+ return `http://${sandboxId}-${port}.localhost:${config.proxyPort}/`;
151
+ }
152
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/cost.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Config } from "./config.js";
2
+ import type { CostBreakdown, SandboxUsage } from "./types.js";
3
+ /**
4
+ * Translate cumulative usage into a cost breakdown using the configured rates.
5
+ * CPU is exact (integrated vCPU-seconds), memory is integrated GB-seconds, and
6
+ * egress is the preview-proxy bytes the sandbox has sent out. (Full internet
7
+ * egress metering arrives with the Phase 3 egress credential proxy.)
8
+ */
9
+ export declare function computeCost(usage: SandboxUsage, config: Config): CostBreakdown;
package/dist/cost.js ADDED
@@ -0,0 +1,19 @@
1
+ const BYTES_PER_GB = 1e9;
2
+ const SECONDS_PER_HOUR = 3600;
3
+ /**
4
+ * Translate cumulative usage into a cost breakdown using the configured rates.
5
+ * CPU is exact (integrated vCPU-seconds), memory is integrated GB-seconds, and
6
+ * egress is the preview-proxy bytes the sandbox has sent out. (Full internet
7
+ * egress metering arrives with the Phase 3 egress credential proxy.)
8
+ */
9
+ export function computeCost(usage, config) {
10
+ const cpu = (usage.cpuSeconds / SECONDS_PER_HOUR) * config.costCpuPerHour;
11
+ const memGbSeconds = usage.memByteSeconds / BYTES_PER_GB;
12
+ const mem = (memGbSeconds / SECONDS_PER_HOUR) * config.costMemGbPerHour;
13
+ const egress = (usage.egressBytes / BYTES_PER_GB) * config.costEgressPerGb;
14
+ // LLM cost is the provider's own reported figure (e.g. OpenRouter usage.cost) —
15
+ // the source of truth, not a rate we apply.
16
+ const provider = usage.providerCost ?? 0;
17
+ return { cpu, mem, egress, provider, total: cpu + mem + egress + provider };
18
+ }
19
+ //# sourceMappingURL=cost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost.js","sourceRoot":"","sources":["../src/cost.ts"],"names":[],"mappings":"AAGA,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAmB,EAAE,MAAc;IAC7D,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC;IAC1E,MAAM,YAAY,GAAG,KAAK,CAAC,cAAc,GAAG,YAAY,CAAC;IACzD,MAAM,GAAG,GAAG,CAAC,YAAY,GAAG,gBAAgB,CAAC,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACxE,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,WAAW,GAAG,YAAY,CAAC,GAAG,MAAM,CAAC,eAAe,CAAC;IAC3E,gFAAgF;IAChF,4CAA4C;IAC5C,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,IAAI,CAAC,CAAC;IACzC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,GAAG,GAAG,MAAM,GAAG,QAAQ,EAAE,CAAC;AAC9E,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Firecracker density benchmark (plan §Verification 4): launch N small microVMs
3
+ * on one host, prove they're all live and functional, and measure what density
4
+ * actually costs — boot latency distribution, host memory per VM, teardown.
5
+ *
6
+ * Run on a KVM host: SBX_DENSITY_N=40 npm run bench:density
7
+ * (image conversion must be cached — run smoke:fc once first).
8
+ */
9
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
10
+ import { homedir, tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { FirecrackerDriver } from "./driver/firecracker.js";
13
+ const N = Number(process.env.SBX_DENSITY_N ?? 40);
14
+ const CONCURRENCY = Number(process.env.SBX_DENSITY_CONCURRENCY ?? 4);
15
+ const MEM_MB = Number(process.env.SBX_DENSITY_MEM_MB ?? 256);
16
+ function hostAvailableMb() {
17
+ const m = /MemAvailable:\s+(\d+)\s+kB/.exec(readFileSync("/proc/meminfo", "utf8"));
18
+ return m ? Math.round(Number(m[1]) / 1024) : 0;
19
+ }
20
+ function pct(sorted, p) {
21
+ return sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))] ?? 0;
22
+ }
23
+ async function main() {
24
+ const stateDir = mkdtempSync(join(tmpdir(), "hotcell-density-"));
25
+ const image = process.env.SBX_FC_SMOKE_IMAGE ?? "alpine:3.20";
26
+ const driver = new FirecrackerDriver({
27
+ fcBin: process.env.SBX_FC_BIN ?? "firecracker",
28
+ kernel: process.env.SBX_FC_KERNEL ?? "helpers/hotcell-vz/guest/vmlinux-fc",
29
+ rootfs: "helpers/hotcell-vz/guest/rootfs.img",
30
+ stateDir,
31
+ diskGb: 1,
32
+ imageCacheDir: process.env.SBX_FC_IMAGE_CACHE ?? join(homedir(), ".sbx", "fc", "images"),
33
+ });
34
+ const ids = Array.from({ length: N }, (_, i) => `dens${String(i).padStart(3, "0")}`);
35
+ const bootMs = [];
36
+ const failures = [];
37
+ const memBefore = hostAvailableMb();
38
+ const t0 = Date.now();
39
+ console.error(`[density] booting ${N} microVMs (${MEM_MB}MB cap each, concurrency ${CONCURRENCY})…`);
40
+ let next = 0;
41
+ const worker = async () => {
42
+ while (next < ids.length) {
43
+ const id = ids[next++];
44
+ const t = Date.now();
45
+ try {
46
+ await driver.create({ id, image, env: {}, persist: true, limits: { memoryMb: MEM_MB, cpus: 1 } });
47
+ bootMs.push(Date.now() - t);
48
+ }
49
+ catch (err) {
50
+ failures.push(`${id}: ${err.message.slice(0, 120)}`);
51
+ }
52
+ }
53
+ };
54
+ await Promise.all(Array.from({ length: CONCURRENCY }, worker));
55
+ const bootWallS = ((Date.now() - t0) / 1000).toFixed(1);
56
+ const memAfter = hostAvailableMb();
57
+ // Every VM must be live and functional, not just booted.
58
+ console.error("[density] exec in every VM…");
59
+ let functional = 0;
60
+ for (const id of ids) {
61
+ if (failures.some((f) => f.startsWith(id)))
62
+ continue;
63
+ try {
64
+ let out = "";
65
+ const code = await driver.exec(id, "echo ok-$HOSTNAME", {}, (e) => {
66
+ if (e.type === "stdout")
67
+ out += e.data;
68
+ });
69
+ if (code === 0 && /ok-/.test(out))
70
+ functional++;
71
+ }
72
+ catch {
73
+ failures.push(`${id}: exec failed post-boot`);
74
+ }
75
+ }
76
+ const booted = bootMs.length;
77
+ const perVmMb = booted > 0 ? Math.round((memBefore - memAfter) / booted) : 0;
78
+ bootMs.sort((a, b) => a - b);
79
+ console.log(`\ndensity-bench: ${booted}/${N} booted, ${functional} functional, ${failures.length} failures`);
80
+ console.log(` boot latency: p50 ${pct(bootMs, 50)}ms / p95 ${pct(bootMs, 95)}ms / max ${pct(bootMs, 100)}ms`);
81
+ console.log(` wall time to boot all: ${bootWallS}s`);
82
+ console.log(` host memory: ${memBefore}MB → ${memAfter}MB available (~${perVmMb}MB per live VM)`);
83
+ for (const f of failures.slice(0, 5))
84
+ console.log(` ✗ ${f}`);
85
+ console.error("[density] tearing down…");
86
+ const tDown = Date.now();
87
+ for (const id of ids)
88
+ await driver.destroy(id).catch(() => { });
89
+ console.log(` teardown: ${((Date.now() - tDown) / 1000).toFixed(1)}s; host available now ${hostAvailableMb()}MB`);
90
+ rmSync(stateDir, { recursive: true, force: true });
91
+ return failures.length === 0 && functional === booted && booted === N ? 0 : 1;
92
+ }
93
+ main().then((code) => process.exit(code), (err) => {
94
+ console.error("density-bench FAILED:", err);
95
+ process.exit(1);
96
+ });
97
+ //# sourceMappingURL=density-bench.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"density-bench.js","sourceRoot":"","sources":["../src/density-bench.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;AAClD,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,CAAC,CAAC,CAAC;AACrE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,GAAG,CAAC,CAAC;AAE7D,SAAS,eAAe;IACtB,MAAM,CAAC,GAAG,4BAA4B,CAAC,IAAI,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;IACnF,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,SAAS,GAAG,CAAC,MAAgB,EAAE,CAAS;IACtC,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACzF,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,aAAa,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,iBAAiB,CAAC;QACnC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,aAAa;QAC9C,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,qCAAqC;QAC1E,MAAM,EAAE,qCAAqC;QAC7C,QAAQ;QACR,MAAM,EAAE,CAAC;QACT,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC;KACzF,CAAC,CAAC;IAEH,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IACrF,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,eAAe,EAAE,CAAC;IACpC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEtB,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC,cAAc,MAAM,4BAA4B,WAAW,IAAI,CAAC,CAAC;IACrG,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,MAAM,GAAG,KAAK,IAAmB,EAAE;QACvC,OAAO,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;YACzB,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;YACvB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACrB,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;gBAClG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;YAC9B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,KAAM,GAAa,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YAClE,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/D,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC;IAEnC,yDAAyD;IACzD,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;IAC7C,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;YAAE,SAAS;QACrD,IAAI,CAAC;YACH,IAAI,GAAG,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,mBAAmB,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE;gBAChE,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;oBAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC;YACzC,CAAC,CAAC,CAAC;YACH,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,UAAU,EAAE,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,yBAAyB,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,SAAS,GAAG,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,OAAO,CAAC,GAAG,CAAC,oBAAoB,MAAM,IAAI,CAAC,YAAY,UAAU,gBAAgB,QAAQ,CAAC,MAAM,WAAW,CAAC,CAAC;IAC7G,OAAO,CAAC,GAAG,CAAC,uBAAuB,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,YAAY,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,YAAY,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/G,OAAO,CAAC,GAAG,CAAC,4BAA4B,SAAS,GAAG,CAAC,CAAC;IACtD,OAAO,CAAC,GAAG,CAAC,kBAAkB,SAAS,QAAQ,QAAQ,kBAAkB,OAAO,iBAAiB,CAAC,CAAC;IACnG,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;QAAE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAE9D,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,KAAK,MAAM,EAAE,IAAI,GAAG;QAAE,MAAM,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC/D,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,yBAAyB,eAAe,EAAE,IAAI,CAAC,CAAC;IACnH,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAEnD,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,KAAK,MAAM,IAAI,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChF,CAAC;AAED,IAAI,EAAE,CAAC,IAAI,CACT,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAC5B,CAAC,GAAG,EAAE,EAAE;IACN,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CACF,CAAC"}
@@ -0,0 +1,67 @@
1
+ import type { AgentConn } from "./agent.js";
2
+ import { UnsupportedDriver } from "./unsupported.js";
3
+ import type { ProcessLiveness, SandboxStats, StartProcessResult, TcpBridge, TerminalOptions, TerminalSession } from "./types.js";
4
+ import type { ExecEvent, ExecOptions, FileChangeEvent, FileInfo, ListFilesOptions, MkdirOptions, ReadFileOptions, StartProcessOptions, WaitForPortOptions, WatchOptions, WriteFileOptions } from "../types.js";
5
+ /**
6
+ * Shared base for microVM drivers whose in-guest work is served by the `sbx-agent`
7
+ * over the A.4 wire protocol (`AgentConn`). Every agent-served `Driver` method —
8
+ * exec, files, watch, processes, preview bridge, terminal, stats, backup/restore —
9
+ * is implemented here **once**, in terms of a single abstract `openConn(id)` that
10
+ * each concrete driver provides for its transport (Apple VZ relays a unix socket
11
+ * to the guest vsock; Firecracker connects to a vsock UDS with a CONNECT
12
+ * handshake). Concrete drivers only implement lifecycle (`create`/`stop`/`start`/
13
+ * `destroy`), `openConn`, and the host probes (`ping`/`hostInfo`) — the whole
14
+ * point of the abstraction is that the surface above the driver is identical.
15
+ */
16
+ export declare abstract class AgentDriver extends UnsupportedDriver {
17
+ /**
18
+ * Open a fresh agent connection to a running sandbox, resolved once the agent's
19
+ * Hello arrives. Throws `<name>: sandbox <id> is not running` if it isn't live.
20
+ * The connection is single-use unless the caller keeps it (long-lived streams).
21
+ */
22
+ protected abstract openConn(id: string): Promise<AgentConn>;
23
+ /** Open a connection, run `fn`, always close it. For one-shot request/response ops. */
24
+ protected withConn<T>(id: string, fn: (c: AgentConn) => Promise<T>): Promise<T>;
25
+ exec(id: string, command: string, opts: ExecOptions, onEvent: (e: ExecEvent) => void): Promise<number>;
26
+ writeFile(id: string, opts: WriteFileOptions): Promise<void>;
27
+ readFile(id: string, opts: ReadFileOptions): Promise<string>;
28
+ mkdir(id: string, opts: MkdirOptions): Promise<void>;
29
+ listFiles(id: string, opts: ListFilesOptions): Promise<FileInfo[]>;
30
+ waitForPort(id: string, port: number, opts: WaitForPortOptions): Promise<boolean>;
31
+ /** Watch a path recursively for changes over the agent (poll-based mtime diff),
32
+ * streaming events until `opts.signal` aborts. Holds a connection for its life. */
33
+ watchFiles(id: string, path: string, opts: WatchOptions & {
34
+ signal: AbortSignal;
35
+ }, onEvent: (e: FileChangeEvent) => void): Promise<void>;
36
+ startProcess(id: string, procId: string, command: string, opts: StartProcessOptions): Promise<StartProcessResult>;
37
+ listProcesses(id: string, procs: Array<{
38
+ procId: string;
39
+ pid: number;
40
+ }>): Promise<ProcessLiveness[]>;
41
+ killProcess(id: string, pid: number, signal?: string): Promise<void>;
42
+ streamProcessLogs(id: string, logPath: string, opts: {
43
+ follow: boolean;
44
+ signal: AbortSignal;
45
+ }, onData: (chunk: string) => void): Promise<void>;
46
+ /** Raw byte bridge to `host:port` inside the guest — backs the preview-URL proxy.
47
+ * Each bridge holds its own connection (the duplex is long-lived). */
48
+ openTcpBridge(id: string, port: number, host: string): Promise<TcpBridge>;
49
+ /** Interactive PTY shell — backs the dashboard/CLI terminal. */
50
+ openTerminal(id: string, opts: TerminalOptions): Promise<TerminalSession>;
51
+ /** Live resource snapshot from the guest's /proc + cgroup files (via the agent).
52
+ * cpuPercent is left 0 by the guest — the metrics sampler derives it from the
53
+ * delta of successive cpuTotalUsageNs samples, exactly as for the container driver. */
54
+ stats(id: string): Promise<SandboxStats>;
55
+ /** Tar the guest's /workspace to `tarPath` on the host (over the agent). */
56
+ createBackup(id: string, tarPath: string): Promise<{
57
+ bytes: number;
58
+ }>;
59
+ /** Replace the guest's /workspace with the tar at `tarPath` (cleared first). */
60
+ restoreBackup(id: string, tarPath: string): Promise<void>;
61
+ /** Run a command and collect its full stdout/stderr/exit (for the process shims). */
62
+ protected execCapture(id: string, command: string, opts?: ExecOptions): Promise<{
63
+ stdout: string;
64
+ stderr: string;
65
+ exitCode: number;
66
+ }>;
67
+ }