@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
package/dist/logger.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Tiny dependency-free structured logger. Emits either one JSON object per line
3
+ * (machine-friendly, the Phase 2 "structured logs" deliverable) or a coloured
4
+ * human line for local dev. A single process-wide logger is configured at
5
+ * startup via `configureLogger`; modules import `log` and use it directly so we
6
+ * don't thread a logger through every call site.
7
+ */
8
+ const LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
9
+ class JsonLogger {
10
+ level;
11
+ format;
12
+ base;
13
+ constructor(level, format, base = {}) {
14
+ this.level = level;
15
+ this.format = format;
16
+ this.base = base;
17
+ }
18
+ child(fields) {
19
+ return new JsonLogger(this.level, this.format, { ...this.base, ...fields });
20
+ }
21
+ debug(msg, fields) {
22
+ this.emit("debug", msg, fields);
23
+ }
24
+ info(msg, fields) {
25
+ this.emit("info", msg, fields);
26
+ }
27
+ warn(msg, fields) {
28
+ this.emit("warn", msg, fields);
29
+ }
30
+ error(msg, fields) {
31
+ this.emit("error", msg, fields);
32
+ }
33
+ emit(level, msg, fields) {
34
+ if (LEVEL_RANK[level] < LEVEL_RANK[this.level])
35
+ return;
36
+ const merged = { ...this.base, ...fields };
37
+ // Logs go to stderr so they never intermix with a command's piped stdout.
38
+ if (this.format === "json") {
39
+ process.stderr.write(JSON.stringify({ ts: new Date().toISOString(), level, msg, ...merged }) + "\n");
40
+ return;
41
+ }
42
+ const ts = new Date().toISOString().slice(11, 23);
43
+ const tag = PRETTY_TAG[level];
44
+ const extra = Object.entries(merged)
45
+ .map(([k, v]) => `${k}=${formatVal(v)}`)
46
+ .join(" ");
47
+ process.stderr.write(`${ts} ${tag} ${msg}${extra ? " " + extra : ""}\n`);
48
+ }
49
+ }
50
+ const PRETTY_TAG = {
51
+ debug: "\x1b[90mDBG\x1b[0m",
52
+ info: "\x1b[32mINF\x1b[0m",
53
+ warn: "\x1b[33mWRN\x1b[0m",
54
+ error: "\x1b[31mERR\x1b[0m",
55
+ };
56
+ function formatVal(v) {
57
+ if (typeof v === "string")
58
+ return /\s/.test(v) ? JSON.stringify(v) : v;
59
+ return JSON.stringify(v);
60
+ }
61
+ /** Process-wide logger. Replaced by `configureLogger`; defaults are quiet-ish. */
62
+ export let log = new JsonLogger("info", "pretty");
63
+ export function configureLogger(opts) {
64
+ log = new JsonLogger(opts.level, opts.format);
65
+ return log;
66
+ }
67
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { Driver } from "./driver/types.js";
2
+ import type { SandboxStore } from "./store.js";
3
+ /** One point in a sandbox's live-metrics history (for dashboard sparklines). */
4
+ export interface MetricSample {
5
+ at: string;
6
+ cpuPercent: number;
7
+ memBytes: number;
8
+ netRxBytes: number;
9
+ netTxBytes: number;
10
+ pids: number;
11
+ }
12
+ /**
13
+ * In-memory ring of recent live samples per sandbox — backs the dashboard's CPU
14
+ * and memory sparklines (`GET /sandboxes/:id/metrics/history`). Deliberately not
15
+ * persisted: it's a short rolling window, cheap to rebuild after a restart.
16
+ */
17
+ export declare class MetricsHistory {
18
+ private readonly cap;
19
+ private readonly byId;
20
+ constructor(cap?: number);
21
+ record(id: string, sample: MetricSample): void;
22
+ get(id: string): MetricSample[];
23
+ /** Most-recently-sampled resident memory (bytes) for a sandbox, if any. */
24
+ latestMemBytes(id: string): number | undefined;
25
+ clear(id: string): void;
26
+ }
27
+ /**
28
+ * In-process metrics sampler — the cAdvisor-style collection the plan calls for,
29
+ * minus a per-sandbox sidecar. On each tick it snapshots every running sandbox's
30
+ * stats and integrates cumulative usage:
31
+ * - CPU: the delta of the container's cumulative CPU-ns (reset-safe — a drop
32
+ * means the container was recreated, so the new total is counted from zero).
33
+ * - Memory: current resident bytes × wall-clock seconds since the last sample.
34
+ * Totals are persisted via `store.setUsage`, so they survive a daemon restart.
35
+ */
36
+ export declare function sampleUsage(driver: Driver, store: SandboxStore, history?: MetricsHistory, now?: number): Promise<void>;
37
+ /**
38
+ * Start the periodic metrics sampler. Returns the interval handle (unref'd so it
39
+ * won't keep the process alive); pass it to `clearInterval` on shutdown.
40
+ */
41
+ export declare function startSampler(opts: {
42
+ driver: Driver;
43
+ store: SandboxStore;
44
+ intervalMs: number;
45
+ history?: MetricsHistory;
46
+ }): NodeJS.Timeout;
@@ -0,0 +1,92 @@
1
+ import { log } from "./logger.js";
2
+ /**
3
+ * In-memory ring of recent live samples per sandbox — backs the dashboard's CPU
4
+ * and memory sparklines (`GET /sandboxes/:id/metrics/history`). Deliberately not
5
+ * persisted: it's a short rolling window, cheap to rebuild after a restart.
6
+ */
7
+ export class MetricsHistory {
8
+ cap;
9
+ byId = new Map();
10
+ constructor(cap = 60) {
11
+ this.cap = cap;
12
+ }
13
+ record(id, sample) {
14
+ let arr = this.byId.get(id);
15
+ if (!arr) {
16
+ arr = [];
17
+ this.byId.set(id, arr);
18
+ }
19
+ arr.push(sample);
20
+ while (arr.length > this.cap)
21
+ arr.shift();
22
+ }
23
+ get(id) {
24
+ return this.byId.get(id) ?? [];
25
+ }
26
+ /** Most-recently-sampled resident memory (bytes) for a sandbox, if any. */
27
+ latestMemBytes(id) {
28
+ const arr = this.byId.get(id);
29
+ return arr && arr.length ? arr[arr.length - 1].memBytes : undefined;
30
+ }
31
+ clear(id) {
32
+ this.byId.delete(id);
33
+ }
34
+ }
35
+ /**
36
+ * In-process metrics sampler — the cAdvisor-style collection the plan calls for,
37
+ * minus a per-sandbox sidecar. On each tick it snapshots every running sandbox's
38
+ * stats and integrates cumulative usage:
39
+ * - CPU: the delta of the container's cumulative CPU-ns (reset-safe — a drop
40
+ * means the container was recreated, so the new total is counted from zero).
41
+ * - Memory: current resident bytes × wall-clock seconds since the last sample.
42
+ * Totals are persisted via `store.setUsage`, so they survive a daemon restart.
43
+ */
44
+ export async function sampleUsage(driver, store, history, now = Date.now()) {
45
+ for (const record of store.list()) {
46
+ if (record.status !== "running")
47
+ continue;
48
+ try {
49
+ const s = await driver.stats(record.id);
50
+ const prev = record.usage;
51
+ history?.record(record.id, {
52
+ at: new Date(now).toISOString(),
53
+ cpuPercent: s.cpuPercent,
54
+ memBytes: s.memBytes,
55
+ netRxBytes: s.netRxBytes,
56
+ netTxBytes: s.netTxBytes,
57
+ pids: s.pids,
58
+ });
59
+ let cpuDeltaNs = s.cpuTotalUsageNs - prev.lastCpuTotalNs;
60
+ if (cpuDeltaNs < 0)
61
+ cpuDeltaNs = s.cpuTotalUsageNs; // container recreated
62
+ const lastMs = prev.lastSampledAt ? Date.parse(prev.lastSampledAt) : now;
63
+ const dtSeconds = Math.max(0, (now - lastMs) / 1000);
64
+ const next = {
65
+ // egress + provider counters are metered by the proxies, not the sampler.
66
+ ...prev,
67
+ cpuSeconds: prev.cpuSeconds + cpuDeltaNs / 1e9,
68
+ memByteSeconds: prev.memByteSeconds + s.memBytes * dtSeconds,
69
+ lastCpuTotalNs: s.cpuTotalUsageNs,
70
+ lastSampledAt: new Date(now).toISOString(),
71
+ };
72
+ store.setUsage(record.id, next);
73
+ }
74
+ catch (err) {
75
+ // A sandbox can race a stop/destroy mid-tick; skip it and move on.
76
+ log.debug("metrics sample failed", { sandbox: record.id, error: String(err) });
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Start the periodic metrics sampler. Returns the interval handle (unref'd so it
82
+ * won't keep the process alive); pass it to `clearInterval` on shutdown.
83
+ */
84
+ export function startSampler(opts) {
85
+ const { driver, store, intervalMs, history } = opts;
86
+ const timer = setInterval(() => {
87
+ sampleUsage(driver, store, history).catch((err) => log.error("sampler error", { error: String(err) }));
88
+ }, intervalMs);
89
+ timer.unref?.();
90
+ return timer;
91
+ }
92
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { Config } from "../config.js";
2
+ /** Derive the bridge gateway IP (`.1`) from a `/24`-style subnet base. */
3
+ export declare function gatewayOf(subnet: string): string;
4
+ export declare function ensureEgressFirewall(config: Config): Promise<void>;
@@ -0,0 +1,80 @@
1
+ import { execFile } from "node:child_process";
2
+ import { platform } from "node:os";
3
+ import { promisify } from "node:util";
4
+ import { log } from "../logger.js";
5
+ const exec = promisify(execFile);
6
+ /**
7
+ * Host-side default-deny egress firewall for sandboxes (Linux only).
8
+ *
9
+ * When `SBX_EGRESS_ENFORCE=true`, sandboxes run on a dedicated bridge network
10
+ * (`config.egressSubnet`). This installs `iptables` rules in the `DOCKER-USER`
11
+ * chain — the supported, operator-owned hook Docker consults before its own rules
12
+ * — so traffic from that subnet may reach ONLY the egress gateway (the bridge
13
+ * gateway `.1` on `egressPort`) and the pinned DNS resolver; everything else is
14
+ * dropped. The sandbox holds no `CAP_NET_ADMIN`, so it cannot edit these rules.
15
+ *
16
+ * Rules live in a dedicated `SBX-EGRESS` chain that is flushed + rebuilt on every
17
+ * startup (idempotent), with a single jump from `DOCKER-USER`.
18
+ *
19
+ * NOT a hard isolation boundary: a kernel-shared container is weaker than a VM,
20
+ * and on Docker Desktop (macOS) `DOCKER-USER` isn't host-editable — there
21
+ * enforcement degrades to advisory (proxy env is still set, but direct egress is
22
+ * not blocked). Transparent, in-guest-enforced default-deny is the microVM driver's
23
+ * job; this is the best that plain Docker on Linux can do. Requires the daemon to
24
+ * run with iptables privileges (root or `CAP_NET_ADMIN`).
25
+ */
26
+ const CHAIN = "SBX-EGRESS";
27
+ /** Derive the bridge gateway IP (`.1`) from a `/24`-style subnet base. */
28
+ export function gatewayOf(subnet) {
29
+ const base = subnet.split("/")[0] ?? subnet;
30
+ const octets = base.split(".");
31
+ if (octets.length !== 4)
32
+ return base;
33
+ octets[3] = "1";
34
+ return octets.join(".");
35
+ }
36
+ export async function ensureEgressFirewall(config) {
37
+ if (!config.egressEnforce)
38
+ return;
39
+ if (platform() !== "linux") {
40
+ log.warn("egress enforcement requested but host is not Linux — fail-closed firewall is ADVISORY only " +
41
+ "(proxy env is injected, but direct egress is NOT blocked). Use the microVM driver for hard isolation.", { platform: platform() });
42
+ return;
43
+ }
44
+ const subnet = config.egressSubnet;
45
+ const gateway = gatewayOf(subnet);
46
+ const port = String(config.egressPort);
47
+ const dns = config.egressDnsResolver;
48
+ // The ordered rule set for the SBX-EGRESS chain. Allows return traffic, the
49
+ // gateway, and DNS (to the pinned resolver if set, else the bridge gateway, which
50
+ // Docker's embedded resolver forwards through); drops the rest.
51
+ const rules = [
52
+ ["-m", "state", "--state", "ESTABLISHED,RELATED", "-j", "ACCEPT"],
53
+ ["-p", "tcp", "-d", gateway, "--dport", port, "-j", "ACCEPT"],
54
+ ["-p", "udp", "-d", dns || gateway, "--dport", "53", "-j", "ACCEPT"],
55
+ ["-p", "tcp", "-d", dns || gateway, "--dport", "53", "-j", "ACCEPT"],
56
+ ["-j", "DROP"],
57
+ ];
58
+ try {
59
+ // Create (ignore "exists"), then flush so we rebuild cleanly each boot.
60
+ await ipt(["-N", CHAIN]).catch(() => { });
61
+ await ipt(["-F", CHAIN]);
62
+ for (const rule of rules)
63
+ await ipt(["-A", CHAIN, ...rule]);
64
+ // Jump from DOCKER-USER for our subnet, exactly once.
65
+ const jump = ["-s", subnet, "-j", CHAIN];
66
+ const present = await ipt(["-C", "DOCKER-USER", ...jump]).then(() => true, () => false);
67
+ if (!present)
68
+ await ipt(["-I", "DOCKER-USER", ...jump]);
69
+ log.info("egress firewall installed (default-deny)", { subnet, gateway, port, dns: dns || "embedded" });
70
+ }
71
+ catch (err) {
72
+ log.error("egress firewall: failed to install iptables rules — egress is NOT locked down. " +
73
+ "Run the daemon with root / CAP_NET_ADMIN, or disable SBX_EGRESS_ENFORCE.", { error: String(err?.message ?? err) });
74
+ }
75
+ }
76
+ /** Run an `iptables` command, throwing with stderr on failure. */
77
+ async function ipt(args) {
78
+ await exec("iptables", ["-w", ...args]);
79
+ }
80
+ //# sourceMappingURL=firewall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firewall.js","sourceRoot":"","sources":["../../src/net/firewall.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Model price table — USD per million tokens, input/output. Used to compute an
3
+ * LLM call's cost when the provider does **not** report one itself (OpenAI,
4
+ * Anthropic, Google all return token counts but no dollar figure; only OpenRouter
5
+ * inlines `usage.cost`). When a provider *does* report cost, that figure is
6
+ * authoritative and this table is not consulted — see `proxy/egress.ts`.
7
+ *
8
+ * Prices are a best-effort default snapshot and are meant to be overridden in
9
+ * production via `SBX_MODEL_PRICES` (a JSON file of the same shape). Keys are
10
+ * lower-cased; lookup also tries prefix matches so dated aliases
11
+ * (`gpt-4o-2024-08-06` → `gpt-4o`) resolve without an exact entry.
12
+ */
13
+ export interface ModelPrice {
14
+ /** USD per 1M input/prompt tokens. */
15
+ inputPerMTok: number;
16
+ /** USD per 1M output/completion tokens. */
17
+ outputPerMTok: number;
18
+ }
19
+ export type ModelPrices = Record<string, ModelPrice>;
20
+ /** Built-in default prices (USD / 1M tokens). Override via `SBX_MODEL_PRICES`. */
21
+ export declare const DEFAULT_MODEL_PRICES: ModelPrices;
22
+ /**
23
+ * Load the model price table: built-in defaults overlaid with a JSON file at
24
+ * `path` (the `SBX_MODEL_PRICES` config), so operators can reprice without a
25
+ * rebuild. A missing/unreadable/malformed file logs a warning and falls back to
26
+ * defaults — pricing must never crash the daemon.
27
+ */
28
+ export declare function loadModelPrices(path: string): ModelPrices;
29
+ /** Resolve a model's price: exact (lower-cased) match, else longest prefix match. */
30
+ export declare function priceFor(model: string, prices: ModelPrices): ModelPrice | undefined;
31
+ /**
32
+ * Compute an LLM call's USD cost from token counts. Returns 0 (and warns once per
33
+ * unknown model) when the model isn't priced — so a gap is observable rather than
34
+ * silently billed as free.
35
+ */
36
+ export declare function computeModelCost(model: string | undefined, tokensIn: number, tokensOut: number, prices: ModelPrices): number;
@@ -0,0 +1,81 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { log } from "./logger.js";
3
+ /** Built-in default prices (USD / 1M tokens). Override via `SBX_MODEL_PRICES`. */
4
+ export const DEFAULT_MODEL_PRICES = {
5
+ // OpenAI
6
+ "gpt-4o": { inputPerMTok: 2.5, outputPerMTok: 10 },
7
+ "gpt-4o-mini": { inputPerMTok: 0.15, outputPerMTok: 0.6 },
8
+ "gpt-4.1": { inputPerMTok: 2, outputPerMTok: 8 },
9
+ "gpt-4.1-mini": { inputPerMTok: 0.4, outputPerMTok: 1.6 },
10
+ "gpt-4.1-nano": { inputPerMTok: 0.1, outputPerMTok: 0.4 },
11
+ "o3": { inputPerMTok: 2, outputPerMTok: 8 },
12
+ "o4-mini": { inputPerMTok: 1.1, outputPerMTok: 4.4 },
13
+ // Anthropic
14
+ "claude-3-5-sonnet": { inputPerMTok: 3, outputPerMTok: 15 },
15
+ "claude-3-5-haiku": { inputPerMTok: 0.8, outputPerMTok: 4 },
16
+ "claude-3-7-sonnet": { inputPerMTok: 3, outputPerMTok: 15 },
17
+ "claude-3-opus": { inputPerMTok: 15, outputPerMTok: 75 },
18
+ "claude-sonnet-4": { inputPerMTok: 3, outputPerMTok: 15 },
19
+ "claude-opus-4": { inputPerMTok: 15, outputPerMTok: 75 },
20
+ // Google Gemini
21
+ "gemini-1.5-pro": { inputPerMTok: 1.25, outputPerMTok: 5 },
22
+ "gemini-1.5-flash": { inputPerMTok: 0.075, outputPerMTok: 0.3 },
23
+ "gemini-2.0-flash": { inputPerMTok: 0.1, outputPerMTok: 0.4 },
24
+ "gemini-2.5-pro": { inputPerMTok: 1.25, outputPerMTok: 10 },
25
+ };
26
+ /**
27
+ * Load the model price table: built-in defaults overlaid with a JSON file at
28
+ * `path` (the `SBX_MODEL_PRICES` config), so operators can reprice without a
29
+ * rebuild. A missing/unreadable/malformed file logs a warning and falls back to
30
+ * defaults — pricing must never crash the daemon.
31
+ */
32
+ export function loadModelPrices(path) {
33
+ if (!path)
34
+ return { ...DEFAULT_MODEL_PRICES };
35
+ try {
36
+ const overrides = JSON.parse(readFileSync(path, "utf8"));
37
+ const merged = { ...DEFAULT_MODEL_PRICES };
38
+ for (const [name, price] of Object.entries(overrides)) {
39
+ if (price && typeof price.inputPerMTok === "number" && typeof price.outputPerMTok === "number") {
40
+ merged[name.toLowerCase()] = price;
41
+ }
42
+ }
43
+ return merged;
44
+ }
45
+ catch (err) {
46
+ log.warn("model prices: failed to load overrides, using defaults", {
47
+ path,
48
+ error: String(err?.message ?? err),
49
+ });
50
+ return { ...DEFAULT_MODEL_PRICES };
51
+ }
52
+ }
53
+ /** Resolve a model's price: exact (lower-cased) match, else longest prefix match. */
54
+ export function priceFor(model, prices) {
55
+ const key = model.toLowerCase();
56
+ if (prices[key])
57
+ return prices[key];
58
+ let best;
59
+ for (const [name, price] of Object.entries(prices)) {
60
+ if (key.startsWith(name) && (!best || name.length > best.name.length)) {
61
+ best = { name, price };
62
+ }
63
+ }
64
+ return best?.price;
65
+ }
66
+ /**
67
+ * Compute an LLM call's USD cost from token counts. Returns 0 (and warns once per
68
+ * unknown model) when the model isn't priced — so a gap is observable rather than
69
+ * silently billed as free.
70
+ */
71
+ export function computeModelCost(model, tokensIn, tokensOut, prices) {
72
+ if (!model)
73
+ return 0;
74
+ const price = priceFor(model, prices);
75
+ if (!price) {
76
+ log.warn("no price for model — cost computed as 0", { model });
77
+ return 0;
78
+ }
79
+ return (tokensIn / 1e6) * price.inputPerMTok + (tokensOut / 1e6) * price.outputPerMTok;
80
+ }
81
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Curated DEFAULT egress allowlist — the "agents can do real work" baseline.
3
+ * Checked into the repo so the community can audit and PR it. Domain/SNI names
4
+ * only (never IPs). Operators override wholesale with `SBX_ALLOWLIST_FILE`, extend
5
+ * with `SBX_ALLOWLIST_EXTRA`, or drop the riskier `source_control` tier with
6
+ * `SBX_ALLOW_SOURCE_CONTROL=false`.
7
+ *
8
+ * Tiers:
9
+ * - `registries` — package managers + their CDNs. ON by default.
10
+ * - `source_control` — git hosts (github/gitlab/bitbucket). ON by default, but
11
+ * NOTE: a writable git host is also an exfil channel
12
+ * (`git push` to an attacker repo). High-security operators
13
+ * should disable this tier.
14
+ *
15
+ * `deny` is checked before `allow` and lists DNS-over-HTTPS endpoints, so an agent
16
+ * can't tunnel DNS (or arbitrary data) out through a DoH resolver even if a wide
17
+ * allowlist would otherwise permit it.
18
+ */
19
+ export interface AllowlistData {
20
+ deny: string[];
21
+ tiers: Record<string, string[]>;
22
+ }
23
+ export declare const DEFAULT_ALLOWLIST: AllowlistData;
@@ -0,0 +1,53 @@
1
+ export const DEFAULT_ALLOWLIST = {
2
+ deny: [
3
+ // DNS-over-HTTPS resolvers (DNS/data exfil channel).
4
+ "cloudflare-dns.com",
5
+ "mozilla.cloudflare-dns.com",
6
+ "dns.google",
7
+ "dns.google.com",
8
+ "dns.quad9.net",
9
+ "doh.opendns.com",
10
+ "doh.cleanbrowsing.org",
11
+ "*.dns.nextdns.io",
12
+ ],
13
+ tiers: {
14
+ registries: [
15
+ // Python
16
+ "pypi.org",
17
+ "files.pythonhosted.org",
18
+ // Node
19
+ "registry.npmjs.org",
20
+ // Rust
21
+ "crates.io",
22
+ "static.crates.io",
23
+ "index.crates.io",
24
+ // Go
25
+ "proxy.golang.org",
26
+ "sum.golang.org",
27
+ // Ruby
28
+ "rubygems.org",
29
+ "*.rubygems.org",
30
+ // Java / Maven
31
+ "repo.maven.apache.org",
32
+ "repo1.maven.org",
33
+ // PHP / Composer
34
+ "packagist.org",
35
+ "repo.packagist.org",
36
+ // OS packages (base images)
37
+ "deb.debian.org",
38
+ "security.debian.org",
39
+ "archive.ubuntu.com",
40
+ "security.ubuntu.com",
41
+ "ports.ubuntu.com",
42
+ "dl-cdn.alpinelinux.org",
43
+ ],
44
+ source_control: [
45
+ "github.com",
46
+ "codeload.github.com",
47
+ "*.githubusercontent.com",
48
+ "gitlab.com",
49
+ "bitbucket.org",
50
+ ],
51
+ },
52
+ };
53
+ //# sourceMappingURL=allowlist-default.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allowlist-default.js","sourceRoot":"","sources":["../../src/proxy/allowlist-default.ts"],"names":[],"mappings":"AAuBA,MAAM,CAAC,MAAM,iBAAiB,GAAkB;IAC9C,IAAI,EAAE;QACJ,qDAAqD;QACrD,oBAAoB;QACpB,4BAA4B;QAC5B,YAAY;QACZ,gBAAgB;QAChB,eAAe;QACf,iBAAiB;QACjB,uBAAuB;QACvB,kBAAkB;KACnB;IACD,KAAK,EAAE;QACL,UAAU,EAAE;YACV,SAAS;YACT,UAAU;YACV,wBAAwB;YACxB,OAAO;YACP,oBAAoB;YACpB,OAAO;YACP,WAAW;YACX,kBAAkB;YAClB,iBAAiB;YACjB,KAAK;YACL,kBAAkB;YAClB,gBAAgB;YAChB,OAAO;YACP,cAAc;YACd,gBAAgB;YAChB,eAAe;YACf,uBAAuB;YACvB,iBAAiB;YACjB,iBAAiB;YACjB,eAAe;YACf,oBAAoB;YACpB,4BAA4B;YAC5B,gBAAgB;YAChB,qBAAqB;YACrB,oBAAoB;YACpB,qBAAqB;YACrB,kBAAkB;YAClB,wBAAwB;SACzB;QACD,cAAc,EAAE;YACd,YAAY;YACZ,qBAAqB;YACrB,yBAAyB;YACzB,YAAY;YACZ,eAAe;SAChB;KACF;CACF,CAAC"}
@@ -0,0 +1,43 @@
1
+ import type { Config } from "../config.js";
2
+ /**
3
+ * Domain allowlist for the egress control plane's forward-proxy path (the
4
+ * absolute-form HTTP and CONNECT tunnels that carry pip/npm/git/apt traffic).
5
+ *
6
+ * Default-deny: a host is reachable only if it matches the allowlist AND does not
7
+ * match the denylist (which wins). Matching is by **domain/SNI name, not IP** —
8
+ * registries and code hosts hide behind rotating CDN IPs (Fastly, Cloudflare, S3),
9
+ * so an IP allowlist is both unmaintainable and leaky. The host comes from the
10
+ * forward-proxy request target / the CONNECT authority / the TLS SNI, none of
11
+ * which require decrypting traffic, so there is no MITM.
12
+ *
13
+ * The denylist exists so that even an operator who widens the allowlist can't
14
+ * accidentally re-open DNS-over-HTTPS endpoints (a DNS-exfil / allowlist-bypass
15
+ * channel) without explicitly removing the deny entry.
16
+ */
17
+ export interface AllowlistDecision {
18
+ allow: boolean;
19
+ /** Why: `denylist` | `allowlist` | `default-deny`. */
20
+ reason: "denylist" | "allowlist" | "default-deny";
21
+ }
22
+ export declare class Allowlist {
23
+ private allowExact;
24
+ private allowSuffix;
25
+ private denyExact;
26
+ private denySuffix;
27
+ constructor(allow: string[], deny: string[]);
28
+ private add;
29
+ /** Decide whether `host` (may include a port) may be reached. Deny wins. */
30
+ check(host: string): AllowlistDecision;
31
+ private matches;
32
+ /** Hosts on the allowlist (for logging/introspection). */
33
+ size(): number;
34
+ }
35
+ /** Strip a `:port` and trailing dot, lower-case. */
36
+ export declare function normalizeHost(host: string): string;
37
+ /**
38
+ * Build the deployment allowlist: the checked-in defaults (registries always; the
39
+ * `source_control` tier unless `SBX_ALLOW_SOURCE_CONTROL=false`) plus
40
+ * `SBX_ALLOWLIST_EXTRA` hosts, OR — when `SBX_ALLOWLIST_FILE` is set — that file's
41
+ * `{allow, deny}` verbatim (a full override for operators who want total control).
42
+ */
43
+ export declare function loadAllowlist(config: Config): Allowlist;
@@ -0,0 +1,87 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { log } from "../logger.js";
3
+ import { DEFAULT_ALLOWLIST } from "./allowlist-default.js";
4
+ export class Allowlist {
5
+ allowExact = new Set();
6
+ allowSuffix = []; // ".foo.com" — matches apex + any subdomain
7
+ denyExact = new Set();
8
+ denySuffix = [];
9
+ constructor(allow, deny) {
10
+ for (const p of allow)
11
+ this.add(p, this.allowExact, this.allowSuffix);
12
+ for (const p of deny)
13
+ this.add(p, this.denyExact, this.denySuffix);
14
+ }
15
+ add(pattern, exact, suffix) {
16
+ const p = pattern.trim().toLowerCase().replace(/\.$/, "");
17
+ if (!p)
18
+ return;
19
+ if (p.startsWith("*."))
20
+ suffix.push(p.slice(1)); // "*.foo.com" -> ".foo.com"
21
+ else
22
+ exact.add(p);
23
+ }
24
+ /** Decide whether `host` (may include a port) may be reached. Deny wins. */
25
+ check(host) {
26
+ const h = normalizeHost(host);
27
+ if (this.matches(h, this.denyExact, this.denySuffix))
28
+ return { allow: false, reason: "denylist" };
29
+ if (this.matches(h, this.allowExact, this.allowSuffix))
30
+ return { allow: true, reason: "allowlist" };
31
+ return { allow: false, reason: "default-deny" };
32
+ }
33
+ matches(host, exact, suffix) {
34
+ if (exact.has(host))
35
+ return true;
36
+ // "*.foo.com" (stored as ".foo.com") matches subdomains AND the apex "foo.com".
37
+ return suffix.some((s) => host.endsWith(s) || host === s.slice(1));
38
+ }
39
+ /** Hosts on the allowlist (for logging/introspection). */
40
+ size() {
41
+ return this.allowExact.size + this.allowSuffix.length;
42
+ }
43
+ }
44
+ /** Strip a `:port` and trailing dot, lower-case. */
45
+ export function normalizeHost(host) {
46
+ let h = host.trim().toLowerCase();
47
+ // Strip a trailing :port (but not the colons inside an IPv6 literal).
48
+ if (!h.includes("[") && h.lastIndexOf(":") > h.indexOf(".")) {
49
+ h = h.slice(0, h.lastIndexOf(":"));
50
+ }
51
+ else if (h.startsWith("[")) {
52
+ h = h.slice(1, h.indexOf("]"));
53
+ }
54
+ return h.replace(/\.$/, "");
55
+ }
56
+ /**
57
+ * Build the deployment allowlist: the checked-in defaults (registries always; the
58
+ * `source_control` tier unless `SBX_ALLOW_SOURCE_CONTROL=false`) plus
59
+ * `SBX_ALLOWLIST_EXTRA` hosts, OR — when `SBX_ALLOWLIST_FILE` is set — that file's
60
+ * `{allow, deny}` verbatim (a full override for operators who want total control).
61
+ */
62
+ export function loadAllowlist(config) {
63
+ if (config.allowlistFile) {
64
+ try {
65
+ const f = JSON.parse(readFileSync(config.allowlistFile, "utf8"));
66
+ const allow = [...(f.allow ?? []), ...Object.values(f.tiers ?? {}).flat(), ...config.allowlistExtra];
67
+ return new Allowlist(allow, f.deny ?? []);
68
+ }
69
+ catch (err) {
70
+ log.error("allowlist: SBX_ALLOWLIST_FILE unreadable — failing closed (deny all)", {
71
+ path: config.allowlistFile,
72
+ error: String(err?.message ?? err),
73
+ });
74
+ return new Allowlist([], []); // fail closed: deny everything rather than silently open
75
+ }
76
+ }
77
+ const def = DEFAULT_ALLOWLIST;
78
+ const allow = [];
79
+ for (const [name, hosts] of Object.entries(def.tiers)) {
80
+ if (name === "source_control" && !config.allowSourceControl)
81
+ continue;
82
+ allow.push(...hosts);
83
+ }
84
+ allow.push(...config.allowlistExtra);
85
+ return new Allowlist(allow, def.deny);
86
+ }
87
+ //# sourceMappingURL=allowlist.js.map