@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.
- package/dist/agent-check.d.ts +1 -0
- package/dist/agent-check.js +105 -0
- package/dist/agent-check.js.map +1 -0
- package/dist/api/server.d.ts +64 -0
- package/dist/api/server.js +1368 -0
- package/dist/api/server.js.map +1 -0
- package/dist/api/ws.d.ts +14 -0
- package/dist/api/ws.js +214 -0
- package/dist/api/ws.js.map +1 -0
- package/dist/applevz-check.d.ts +1 -0
- package/dist/applevz-check.js +280 -0
- package/dist/applevz-check.js.map +1 -0
- package/dist/backups.d.ts +29 -0
- package/dist/backups.js +76 -0
- package/dist/backups.js.map +1 -0
- package/dist/capacity.d.ts +63 -0
- package/dist/capacity.js +95 -0
- package/dist/capacity.js.map +1 -0
- package/dist/config.d.ts +241 -0
- package/dist/config.js +152 -0
- package/dist/config.js.map +1 -0
- package/dist/cost.d.ts +9 -0
- package/dist/cost.js +19 -0
- package/dist/cost.js.map +1 -0
- package/dist/density-bench.d.ts +1 -0
- package/dist/density-bench.js +97 -0
- package/dist/density-bench.js.map +1 -0
- package/dist/driver/agent-driver.d.ts +67 -0
- package/dist/driver/agent-driver.js +197 -0
- package/dist/driver/agent-driver.js.map +1 -0
- package/dist/driver/agent.d.ts +94 -0
- package/dist/driver/agent.js +319 -0
- package/dist/driver/agent.js.map +1 -0
- package/dist/driver/applevz.d.ts +95 -0
- package/dist/driver/applevz.js +527 -0
- package/dist/driver/applevz.js.map +1 -0
- package/dist/driver/container.d.ts +99 -0
- package/dist/driver/container.js +733 -0
- package/dist/driver/container.js.map +1 -0
- package/dist/driver/fc-api.d.ts +85 -0
- package/dist/driver/fc-api.js +173 -0
- package/dist/driver/fc-api.js.map +1 -0
- package/dist/driver/firecracker.d.ts +126 -0
- package/dist/driver/firecracker.js +560 -0
- package/dist/driver/firecracker.js.map +1 -0
- package/dist/driver/index.d.ts +17 -0
- package/dist/driver/index.js +61 -0
- package/dist/driver/index.js.map +1 -0
- package/dist/driver/router.d.ts +82 -0
- package/dist/driver/router.js +159 -0
- package/dist/driver/router.js.map +1 -0
- package/dist/driver/types.d.ts +234 -0
- package/dist/driver/types.js +2 -0
- package/dist/driver/types.js.map +1 -0
- package/dist/driver/unsupported.d.ts +50 -0
- package/dist/driver/unsupported.js +81 -0
- package/dist/driver/unsupported.js.map +1 -0
- package/dist/driver/vz-image.d.ts +35 -0
- package/dist/driver/vz-image.js +75 -0
- package/dist/driver/vz-image.js.map +1 -0
- package/dist/e2e-harness.d.ts +1 -0
- package/dist/e2e-harness.js +99 -0
- package/dist/e2e-harness.js.map +1 -0
- package/dist/egress-check.d.ts +8 -0
- package/dist/egress-check.js +317 -0
- package/dist/egress-check.js.map +1 -0
- package/dist/fc-check.d.ts +1 -0
- package/dist/fc-check.js +113 -0
- package/dist/fc-check.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +114 -0
- package/dist/index.js.map +1 -0
- package/dist/kernels.d.ts +22 -0
- package/dist/kernels.js +134 -0
- package/dist/kernels.js.map +1 -0
- package/dist/lifecycle.d.ts +49 -0
- package/dist/lifecycle.js +111 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +67 -0
- package/dist/logger.js.map +1 -0
- package/dist/metrics.d.ts +46 -0
- package/dist/metrics.js +92 -0
- package/dist/metrics.js.map +1 -0
- package/dist/net/firewall.d.ts +4 -0
- package/dist/net/firewall.js +80 -0
- package/dist/net/firewall.js.map +1 -0
- package/dist/pricing.d.ts +36 -0
- package/dist/pricing.js +81 -0
- package/dist/pricing.js.map +1 -0
- package/dist/proxy/allowlist-default.d.ts +23 -0
- package/dist/proxy/allowlist-default.js +53 -0
- package/dist/proxy/allowlist-default.js.map +1 -0
- package/dist/proxy/allowlist.d.ts +43 -0
- package/dist/proxy/allowlist.js +87 -0
- package/dist/proxy/allowlist.js.map +1 -0
- package/dist/proxy/egress.d.ts +49 -0
- package/dist/proxy/egress.js +599 -0
- package/dist/proxy/egress.js.map +1 -0
- package/dist/proxy/ratelimit.d.ts +49 -0
- package/dist/proxy/ratelimit.js +74 -0
- package/dist/proxy/ratelimit.js.map +1 -0
- package/dist/proxy/server.d.ts +26 -0
- package/dist/proxy/server.js +202 -0
- package/dist/proxy/server.js.map +1 -0
- package/dist/router-check.d.ts +1 -0
- package/dist/router-check.js +97 -0
- package/dist/router-check.js.map +1 -0
- package/dist/smoke-fc.d.ts +1 -0
- package/dist/smoke-fc.js +183 -0
- package/dist/smoke-fc.js.map +1 -0
- package/dist/smoke-microvm.d.ts +1 -0
- package/dist/smoke-microvm.js +187 -0
- package/dist/smoke-microvm.js.map +1 -0
- package/dist/smoke-remote.d.ts +20 -0
- package/dist/smoke-remote.js +155 -0
- package/dist/smoke-remote.js.map +1 -0
- package/dist/smoke-vz.d.ts +1 -0
- package/dist/smoke-vz.js +161 -0
- package/dist/smoke-vz.js.map +1 -0
- package/dist/smoke.d.ts +7 -0
- package/dist/smoke.js +962 -0
- package/dist/smoke.js.map +1 -0
- package/dist/store.d.ts +124 -0
- package/dist/store.js +578 -0
- package/dist/store.js.map +1 -0
- package/dist/tracing.d.ts +45 -0
- package/dist/tracing.js +144 -0
- package/dist/tracing.js.map +1 -0
- package/dist/types.d.ts +274 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +27 -0
- package/dist/util.js +63 -0
- package/dist/util.js.map +1 -0
- package/dist/warmpool-check.d.ts +1 -0
- package/dist/warmpool-check.js +83 -0
- package/dist/warmpool-check.js.map +1 -0
- package/dist/web/dashboard.d.ts +1 -0
- package/dist/web/dashboard.js +456 -0
- package/dist/web/dashboard.js.map +1 -0
- 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;
|
package/dist/metrics.js
ADDED
|
@@ -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,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;
|
package/dist/pricing.js
ADDED
|
@@ -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
|