@hotcell/daemon 0.1.23 → 0.1.24
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/api/server.d.ts +2 -0
- package/dist/api/server.js +117 -14
- package/dist/api/server.js.map +1 -1
- package/dist/audit-check.d.ts +1 -0
- package/dist/audit-check.js +113 -0
- package/dist/audit-check.js.map +1 -0
- package/dist/audit.d.ts +66 -0
- package/dist/audit.js +260 -0
- package/dist/audit.js.map +1 -0
- package/dist/boot-scheduler.d.ts +25 -0
- package/dist/boot-scheduler.js +81 -0
- package/dist/boot-scheduler.js.map +1 -0
- package/dist/config.d.ts +15 -3
- package/dist/config.js +22 -0
- package/dist/config.js.map +1 -1
- package/dist/driver/container.d.ts +6 -0
- package/dist/driver/container.js +45 -2
- package/dist/driver/container.js.map +1 -1
- package/dist/driver/router.d.ts +5 -0
- package/dist/driver/router.js +6 -0
- package/dist/driver/router.js.map +1 -1
- package/dist/driver/types.d.ts +2 -0
- package/dist/e2e-harness.js +39 -26
- package/dist/e2e-harness.js.map +1 -1
- package/dist/egress-check.js +7 -0
- package/dist/egress-check.js.map +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/lifecycle.js +6 -2
- package/dist/lifecycle.js.map +1 -1
- package/dist/net/firewall.d.ts +26 -1
- package/dist/net/firewall.js +175 -35
- package/dist/net/firewall.js.map +1 -1
- package/dist/proxy/egress.js +25 -0
- package/dist/proxy/egress.js.map +1 -1
- package/dist/smoke.js +8 -0
- package/dist/smoke.js.map +1 -1
- package/dist/tracing.d.ts +1 -0
- package/dist/tracing.js +17 -3
- package/dist/tracing.js.map +1 -1
- package/package.json +2 -2
package/dist/api/server.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { BackupRegistry } from "../backups.js";
|
|
|
3
3
|
import { type Config } from "../config.js";
|
|
4
4
|
import type { Driver } from "../driver/types.js";
|
|
5
5
|
import type { Capacity } from "../capacity.js";
|
|
6
|
+
import type { BootScheduler } from "../boot-scheduler.js";
|
|
6
7
|
import type { MetricsHistory } from "../metrics.js";
|
|
7
8
|
import { SandboxStore } from "../store.js";
|
|
8
9
|
interface Deps {
|
|
@@ -12,6 +13,7 @@ interface Deps {
|
|
|
12
13
|
backups: BackupRegistry;
|
|
13
14
|
history?: MetricsHistory;
|
|
14
15
|
capacity?: Capacity;
|
|
16
|
+
bootScheduler?: BootScheduler;
|
|
15
17
|
/** Re-read provider keys (file/keychain/env) and hot-swap the egress gateway. */
|
|
16
18
|
reloadKeys?: () => {
|
|
17
19
|
providers: number;
|
package/dist/api/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { audit, redactCommandForAudit } from "../audit.js";
|
|
2
3
|
import { previewUrl } from "../config.js";
|
|
3
4
|
import { kernelFor } from "../kernels.js";
|
|
4
5
|
import { pauseSandbox, resumeSandbox } from "../lifecycle.js";
|
|
@@ -133,6 +134,7 @@ async function handleTerminalUpgrade(req, socket, head, deps) {
|
|
|
133
134
|
return;
|
|
134
135
|
}
|
|
135
136
|
if (!isAllowedHost(req, config)) {
|
|
137
|
+
auditAccessDenied("auth.host_rejected", "GET", url.pathname, "host_not_allowed", driver, store);
|
|
136
138
|
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
137
139
|
socket.destroy();
|
|
138
140
|
return;
|
|
@@ -140,6 +142,7 @@ async function handleTerminalUpgrade(req, socket, head, deps) {
|
|
|
140
142
|
if (config.apiKey) {
|
|
141
143
|
const provided = url.searchParams.get("key") ?? bearerFromHeader(req);
|
|
142
144
|
if (!provided || !safeEqual(provided, config.apiKey)) {
|
|
145
|
+
auditAccessDenied("auth.rejected", "GET", url.pathname, "invalid_api_key", driver, store);
|
|
143
146
|
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
144
147
|
socket.destroy();
|
|
145
148
|
return;
|
|
@@ -184,11 +187,13 @@ async function handleTerminalUpgrade(req, socket, head, deps) {
|
|
|
184
187
|
term = await driver.openTerminal(id, { cols, rows, env: record.env });
|
|
185
188
|
}
|
|
186
189
|
catch (err) {
|
|
190
|
+
audit({ event: "sandbox.terminal.open.failed", sandboxId: id, driver: record.driver ?? driver.name });
|
|
187
191
|
ws.send(`\r\n[sbx] failed to open terminal: ${errorMessage(err)}\r\n`);
|
|
188
192
|
ws.close();
|
|
189
193
|
span.setStatus("error").end();
|
|
190
194
|
return;
|
|
191
195
|
}
|
|
196
|
+
audit({ event: "sandbox.terminal.open", sandboxId: id, driver: record.driver ?? driver.name });
|
|
192
197
|
log.info("terminal opened", { sandbox: id, traceId: span.traceId });
|
|
193
198
|
term.stream.on("data", (chunk) => ws.send(chunk));
|
|
194
199
|
term.stream.on("end", () => ws.close());
|
|
@@ -211,6 +216,7 @@ async function handleTerminalUpgrade(req, socket, head, deps) {
|
|
|
211
216
|
});
|
|
212
217
|
ws.onClose(() => {
|
|
213
218
|
term.close();
|
|
219
|
+
audit({ event: "sandbox.terminal.close", sandboxId: id, driver: record.driver ?? driver.name });
|
|
214
220
|
span.end();
|
|
215
221
|
log.info("terminal closed", { sandbox: id });
|
|
216
222
|
});
|
|
@@ -401,7 +407,18 @@ function resolveLimits(body, config) {
|
|
|
401
407
|
function normalizeRoute(path) {
|
|
402
408
|
return path
|
|
403
409
|
.replace(/^\/sandboxes\/[^/]+/, "/sandboxes/:id")
|
|
404
|
-
.replace(/\/(processes|sessions|code-contexts|expose|backups)\/[^/]
|
|
410
|
+
.replace(/\/(processes|sessions|code-contexts|expose|backups|egress-tokens)\/[^/]+/g, "/$1/:sub");
|
|
411
|
+
}
|
|
412
|
+
/** Audit an access rejection without recording credentials or raw path identifiers. */
|
|
413
|
+
function auditAccessDenied(event, method, path, reason, driver, store) {
|
|
414
|
+
const sandboxId = path.match(/^\/sandboxes\/([^/]+)/)?.[1];
|
|
415
|
+
const record = sandboxId ? store.get(sandboxId) : undefined;
|
|
416
|
+
audit({
|
|
417
|
+
event,
|
|
418
|
+
sandboxId,
|
|
419
|
+
driver: record?.driver ?? driver.name,
|
|
420
|
+
attributes: { method, route: normalizeRoute(path), reason },
|
|
421
|
+
});
|
|
405
422
|
}
|
|
406
423
|
/**
|
|
407
424
|
* Enforce the API key when one is configured. `/healthz` and the dashboard HTML
|
|
@@ -459,9 +476,11 @@ async function handle(req, res, deps) {
|
|
|
459
476
|
const path = url.pathname;
|
|
460
477
|
const method = req.method ?? "GET";
|
|
461
478
|
if (!isAllowedHost(req, config)) {
|
|
479
|
+
auditAccessDenied("auth.host_rejected", method, path, "host_not_allowed", driver, store);
|
|
462
480
|
return sendJson(res, 403, { error: "host not allowed" });
|
|
463
481
|
}
|
|
464
482
|
if (!isAuthorized(req, config, path)) {
|
|
483
|
+
auditAccessDenied("auth.rejected", method, path, "invalid_api_key", driver, store);
|
|
465
484
|
res.setHeader("WWW-Authenticate", "Bearer");
|
|
466
485
|
return sendJson(res, 401, { error: "missing or invalid API key" });
|
|
467
486
|
}
|
|
@@ -500,6 +519,8 @@ async function handle(req, res, deps) {
|
|
|
500
519
|
if (!store.get(egressTokenIdMatch[1]))
|
|
501
520
|
return sendJson(res, 404, { error: "not found" });
|
|
502
521
|
const removed = store.removeEgressToken(egressTokenIdMatch[2]);
|
|
522
|
+
if (removed)
|
|
523
|
+
audit({ event: "gateway.token.revoked", sandboxId: egressTokenIdMatch[1], driver: store.get(egressTokenIdMatch[1])?.driver ?? driver.name, attributes: { "token.raw": egressTokenIdMatch[2] } });
|
|
503
524
|
if (!removed)
|
|
504
525
|
return sendJson(res, 404, { error: "token not found" });
|
|
505
526
|
return sendJson(res, 200, { token: egressTokenIdMatch[2], revoked: true });
|
|
@@ -516,6 +537,7 @@ async function handle(req, res, deps) {
|
|
|
516
537
|
return sendJson(res, 400, { error: parsed.error });
|
|
517
538
|
const token = SandboxStore.newEgressToken();
|
|
518
539
|
store.addEgressToken(token, id, parsed.policy);
|
|
540
|
+
audit({ event: "gateway.token.minted", sandboxId: id, driver: store.get(id)?.driver ?? driver.name, attributes: { "token.raw": token, providers: (parsed.policy.providers ?? []).join(",") } });
|
|
519
541
|
return sendJson(res, 201, {
|
|
520
542
|
token,
|
|
521
543
|
policy: parsed.policy,
|
|
@@ -538,13 +560,38 @@ async function handle(req, res, deps) {
|
|
|
538
560
|
if (method === "GET" && path === "/capacity") {
|
|
539
561
|
if (!deps.capacity)
|
|
540
562
|
return sendJson(res, 200, { enforced: false });
|
|
541
|
-
return sendJson(res, 200,
|
|
563
|
+
return sendJson(res, 200, {
|
|
564
|
+
...deps.capacity.snapshot(),
|
|
565
|
+
boot: deps.bootScheduler?.snapshot(),
|
|
566
|
+
});
|
|
542
567
|
}
|
|
543
568
|
// Hot-reload provider keys after `hotcell keys add/rm` — no daemon restart.
|
|
544
569
|
if (method === "POST" && path === "/reload-keys") {
|
|
545
570
|
const r = deps.reloadKeys?.() ?? { providers: 0 };
|
|
546
571
|
return sendJson(res, 200, { ok: true, ...r });
|
|
547
572
|
}
|
|
573
|
+
if (method === "POST" && path === "/audit-events") {
|
|
574
|
+
const body = await readJson(req, config.maxBodyBytes);
|
|
575
|
+
if (body.event !== "keys.import" || !Array.isArray(body.choices) || body.choices.length > 500) {
|
|
576
|
+
return sendJson(res, 400, { error: "invalid audit event payload" });
|
|
577
|
+
}
|
|
578
|
+
const choices = body.choices;
|
|
579
|
+
if (!choices.every((choice) => {
|
|
580
|
+
if (!choice || typeof choice !== "object")
|
|
581
|
+
return false;
|
|
582
|
+
const item = choice;
|
|
583
|
+
return typeof item.name === "string" && /^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(item.name)
|
|
584
|
+
&& (item.disposition === "gateway" || item.disposition === "inject" || item.disposition === "skip");
|
|
585
|
+
}))
|
|
586
|
+
return sendJson(res, 400, { error: "invalid key import choice" });
|
|
587
|
+
const safeChoices = choices;
|
|
588
|
+
audit({ event: "keys.import", driver: driver.name, attributes: {
|
|
589
|
+
"key.count": safeChoices.length,
|
|
590
|
+
"key.names": safeChoices.map((item) => item.name).join(","),
|
|
591
|
+
"key.dispositions": safeChoices.map((item) => `${item.name}=${item.disposition}`).join(","),
|
|
592
|
+
} });
|
|
593
|
+
return sendJson(res, 202, { accepted: true });
|
|
594
|
+
}
|
|
548
595
|
// Recent finished spans, for debugging and the trace view. Newest first.
|
|
549
596
|
if (method === "GET" && path === "/traces") {
|
|
550
597
|
const limit = Number(url.searchParams.get("limit") ?? "100") || 100;
|
|
@@ -557,7 +604,13 @@ async function handle(req, res, deps) {
|
|
|
557
604
|
}
|
|
558
605
|
if (method === "POST" && path === "/sandboxes") {
|
|
559
606
|
const body = await readJson(req, config.maxBodyBytes);
|
|
560
|
-
return createSandbox(res, {
|
|
607
|
+
return createSandbox(res, {
|
|
608
|
+
config,
|
|
609
|
+
driver,
|
|
610
|
+
store,
|
|
611
|
+
capacity: deps.capacity,
|
|
612
|
+
bootScheduler: deps.bootScheduler,
|
|
613
|
+
}, body);
|
|
561
614
|
}
|
|
562
615
|
if (method === "GET" && path === "/sandboxes") {
|
|
563
616
|
return sendJson(res, 200, { sandboxes: store.list() });
|
|
@@ -665,9 +718,16 @@ async function handle(req, res, deps) {
|
|
|
665
718
|
}
|
|
666
719
|
const backupIdMatch = path.match(/^\/backups\/([^/]+)$/);
|
|
667
720
|
if (method === "DELETE" && backupIdMatch) {
|
|
721
|
+
const backupInfo = await backups.get(backupIdMatch[1]);
|
|
668
722
|
const removed = await backups.remove(backupIdMatch[1]);
|
|
669
723
|
if (!removed)
|
|
670
724
|
return sendJson(res, 404, { error: "backup not found" });
|
|
725
|
+
audit({
|
|
726
|
+
event: "sandbox.backup.delete",
|
|
727
|
+
sandboxId: backupInfo?.sandboxId,
|
|
728
|
+
driver: backupInfo ? store.get(backupInfo.sandboxId)?.driver ?? driver.name : driver.name,
|
|
729
|
+
attributes: { backupId: backupIdMatch[1] },
|
|
730
|
+
});
|
|
671
731
|
return sendJson(res, 200, { backupId: backupIdMatch[1], deleted: true });
|
|
672
732
|
}
|
|
673
733
|
const backupsMatch = path.match(/^\/sandboxes\/([^/]+)\/backups$/);
|
|
@@ -750,6 +810,7 @@ async function handle(req, res, deps) {
|
|
|
750
810
|
if (!record)
|
|
751
811
|
return sendJson(res, 404, { error: "not found" });
|
|
752
812
|
await driver.destroy(id);
|
|
813
|
+
audit({ event: "sandbox.destroy", sandboxId: id, driver: record.driver ?? driver.name, attributes: { image: record.image } });
|
|
753
814
|
store.remove(id);
|
|
754
815
|
store.clearSandbox(id);
|
|
755
816
|
deps.history?.clear(id);
|
|
@@ -758,7 +819,10 @@ async function handle(req, res, deps) {
|
|
|
758
819
|
}
|
|
759
820
|
sendJson(res, 404, { error: `no route for ${method} ${path}` });
|
|
760
821
|
}
|
|
761
|
-
|
|
822
|
+
function safeRepoForAudit(repo) {
|
|
823
|
+
return repo.replace(/:\/\/[^/@]+@/g, "://").split(/[?#]/, 1)[0]?.slice(0, 512) ?? "";
|
|
824
|
+
}
|
|
825
|
+
async function createSandbox(res, { config, driver, store, capacity, bootScheduler }, body) {
|
|
762
826
|
const id = SandboxStore.newId();
|
|
763
827
|
const image = typeof body.image === "string" ? body.image : config.defaultImage;
|
|
764
828
|
// Per-sandbox isolation selection: pick the runtime driver (default SBX_DRIVER).
|
|
@@ -801,8 +865,10 @@ async function createSandbox(res, { config, driver, store, capacity }, body) {
|
|
|
801
865
|
ok: true,
|
|
802
866
|
release: () => { },
|
|
803
867
|
};
|
|
804
|
-
if (!admitted.ok)
|
|
868
|
+
if (!admitted.ok) {
|
|
869
|
+
audit({ event: "sandbox.admission_denied", sandboxId: id, driver: driverName, attributes: { image, memoryMb: limits.memoryMb ?? 0, cpus: limits.cpus ?? 0, status: 503, reason: admitted.reason.slice(0, 256) } });
|
|
805
870
|
return sendCapacityRefusal(res, admitted.reason);
|
|
871
|
+
}
|
|
806
872
|
// Egress wiring. `egress: true` (or a policy object) opts the sandbox's LLM SDKs
|
|
807
873
|
// into the gateway (provider base-URL + key env). Independently, when egress
|
|
808
874
|
// enforcement is ON every sandbox is locked to the gateway, so it also gets a
|
|
@@ -878,19 +944,38 @@ async function createSandbox(res, { config, driver, store, capacity }, body) {
|
|
|
878
944
|
usage: emptyUsage(),
|
|
879
945
|
};
|
|
880
946
|
store.add(record);
|
|
947
|
+
audit({ event: "sandbox.create", sandboxId: id, driver: driverName, attributes: { image, repo: repo ? safeRepoForAudit(repo) : "", branch: (repoRef ?? branchRaw ?? "").slice(0, 256), memoryMb: limits.memoryMb ?? 0, cpus: limits.cpus ?? 0 } });
|
|
881
948
|
if (egressToken)
|
|
882
949
|
store.addEgressToken(egressToken, id, egressPolicy);
|
|
883
|
-
const provision = () =>
|
|
884
|
-
id
|
|
885
|
-
|
|
886
|
-
|
|
950
|
+
const provision = () => (driverName === "firecracker" || driverName === "applevz") && bootScheduler
|
|
951
|
+
? bootScheduler.run(`create:${id}`, () => driver.create({
|
|
952
|
+
id, image, driver: driverName, env, labels, persist, setup: setupSteps,
|
|
953
|
+
repo, repoRef, limits, networked, writableRootfs, cpuset,
|
|
954
|
+
onProgress: (phase) => {
|
|
955
|
+
const r = store.get(id);
|
|
956
|
+
if (r?.status === "creating") {
|
|
957
|
+
r.statusReason = phase;
|
|
958
|
+
store.add(r);
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
}), () => {
|
|
887
962
|
const r = store.get(id);
|
|
888
963
|
if (r?.status === "creating") {
|
|
889
|
-
r.statusReason =
|
|
964
|
+
r.statusReason = "queued for microVM boot";
|
|
890
965
|
store.add(r);
|
|
891
966
|
}
|
|
892
|
-
}
|
|
893
|
-
|
|
967
|
+
})
|
|
968
|
+
: driver.create({
|
|
969
|
+
id, image, driver: driverName, env, labels, persist, setup: setupSteps,
|
|
970
|
+
repo, repoRef, limits, networked, writableRootfs, cpuset,
|
|
971
|
+
onProgress: (phase) => {
|
|
972
|
+
const r = store.get(id);
|
|
973
|
+
if (r?.status === "creating") {
|
|
974
|
+
r.statusReason = phase;
|
|
975
|
+
store.add(r);
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
});
|
|
894
979
|
// Terminal transitions. Both re-check the store: a DELETE mid-create is the
|
|
895
980
|
// cancel path, and must win — never resurrect a destroyed record.
|
|
896
981
|
const finishOk = () => {
|
|
@@ -904,6 +989,7 @@ async function createSandbox(res, { config, driver, store, capacity }, body) {
|
|
|
904
989
|
r.statusReason = undefined;
|
|
905
990
|
r.lastActivityAt = new Date().toISOString();
|
|
906
991
|
store.add(r);
|
|
992
|
+
audit({ event: "sandbox.start", sandboxId: id, driver: r.driver ?? driver.name });
|
|
907
993
|
return r;
|
|
908
994
|
};
|
|
909
995
|
const finishFail = async (err, keepRecord) => {
|
|
@@ -958,7 +1044,8 @@ async function stopSandbox(res, { driver, store }, id) {
|
|
|
958
1044
|
// Processes and exposed ports die with the container; sessions persist.
|
|
959
1045
|
store.clearRuntimeState(id);
|
|
960
1046
|
record.status = "stopped";
|
|
961
|
-
store.add(record);
|
|
1047
|
+
store.add(record);
|
|
1048
|
+
audit({ event: "sandbox.stop", sandboxId: id, driver: record.driver ?? driver.name }); // write-through the status change
|
|
962
1049
|
sendJson(res, 200, record);
|
|
963
1050
|
}
|
|
964
1051
|
async function startSandbox(res, { driver, store, capacity }, id) {
|
|
@@ -1097,6 +1184,7 @@ async function createBackup(res, { driver, store, backups, capacity }, id) {
|
|
|
1097
1184
|
bytes,
|
|
1098
1185
|
};
|
|
1099
1186
|
await backups.save(info);
|
|
1187
|
+
audit({ event: "sandbox.backup.create", sandboxId: id, driver: record.driver ?? driver.name, attributes: { backupId, bytes } });
|
|
1100
1188
|
sendJson(res, 201, info);
|
|
1101
1189
|
}
|
|
1102
1190
|
async function restoreBackup(res, { driver, store, backups, capacity }, id, body) {
|
|
@@ -1112,6 +1200,7 @@ async function restoreBackup(res, { driver, store, backups, capacity }, id, body
|
|
|
1112
1200
|
if (!(await ensureLive(res, driver, store, id, capacity)))
|
|
1113
1201
|
return;
|
|
1114
1202
|
await driver.restoreBackup(id, backups.tarPath(backupId));
|
|
1203
|
+
audit({ event: "sandbox.backup.restore", sandboxId: id, driver: store.get(id)?.driver ?? driver.name, attributes: { backupId } });
|
|
1115
1204
|
sendJson(res, 200, { id, restored: backupId });
|
|
1116
1205
|
}
|
|
1117
1206
|
// --- code interpreter ------------------------------------------------------
|
|
@@ -1293,9 +1382,16 @@ async function execInSandbox(res, { driver, store, capacity }, id, body) {
|
|
|
1293
1382
|
// The stream is live work: the reaper must see this sandbox as active for
|
|
1294
1383
|
// the whole run, not just the instant the request arrived.
|
|
1295
1384
|
store.beginExec(id);
|
|
1385
|
+
const execStartedAt = Date.now();
|
|
1386
|
+
audit({ event: "sandbox.exec.start", sandboxId: id, driver: record.driver ?? driver.name, attributes: { command: redactCommandForAudit(command), cwd: cwd ?? "" } });
|
|
1387
|
+
let auditExitCode = 1;
|
|
1296
1388
|
const stopKeepalive = startSseKeepalive(res);
|
|
1297
1389
|
try {
|
|
1298
|
-
await driver.exec(id, runCommand, { cwd, env },
|
|
1390
|
+
await driver.exec(id, runCommand, { cwd, env }, (event) => {
|
|
1391
|
+
if (event.type === "exit")
|
|
1392
|
+
auditExitCode = event.exitCode;
|
|
1393
|
+
write(event);
|
|
1394
|
+
});
|
|
1299
1395
|
if (session && cwdFile) {
|
|
1300
1396
|
try {
|
|
1301
1397
|
const next = (await driver.readFile(id, { path: cwdFile })).trim();
|
|
@@ -1315,6 +1411,7 @@ async function execInSandbox(res, { driver, store, capacity }, id, body) {
|
|
|
1315
1411
|
}
|
|
1316
1412
|
finally {
|
|
1317
1413
|
stopKeepalive();
|
|
1414
|
+
audit({ event: "sandbox.exec", sandboxId: id, driver: record.driver ?? driver.name, attributes: { command: redactCommandForAudit(command), cwd: cwd ?? "", exitCode: auditExitCode, durationMs: Date.now() - execStartedAt } });
|
|
1318
1415
|
store.endExec(id);
|
|
1319
1416
|
res.end();
|
|
1320
1417
|
}
|
|
@@ -1380,14 +1477,17 @@ async function handleFileAction(res, { driver, store, capacity }, id, action, bo
|
|
|
1380
1477
|
content: body.content,
|
|
1381
1478
|
mode: typeof body.mode === "string" ? body.mode : undefined,
|
|
1382
1479
|
});
|
|
1480
|
+
audit({ event: "sandbox.file.write", sandboxId: id, driver: store.get(id)?.driver ?? driver.name, attributes: { path: path.slice(0, 512), bytes: Buffer.byteLength(body.content, "utf8") } });
|
|
1383
1481
|
return sendJson(res, 200, { ok: true });
|
|
1384
1482
|
}
|
|
1385
1483
|
case "read": {
|
|
1386
1484
|
const content = await driver.readFile(id, { path });
|
|
1485
|
+
audit({ event: "sandbox.file.read", sandboxId: id, driver: store.get(id)?.driver ?? driver.name, attributes: { path: path.slice(0, 512), bytes: Buffer.byteLength(content, "utf8") } });
|
|
1387
1486
|
return sendJson(res, 200, { content });
|
|
1388
1487
|
}
|
|
1389
1488
|
case "mkdir": {
|
|
1390
1489
|
await driver.mkdir(id, { path, parents: body.parents === true });
|
|
1490
|
+
audit({ event: "sandbox.file.mkdir", sandboxId: id, driver: store.get(id)?.driver ?? driver.name, attributes: { path: path.slice(0, 512), parents: body.parents === true } });
|
|
1391
1491
|
return sendJson(res, 200, { ok: true });
|
|
1392
1492
|
}
|
|
1393
1493
|
case "list": {
|
|
@@ -1426,6 +1526,7 @@ async function startProcess(res, { driver, store, capacity }, id, body) {
|
|
|
1426
1526
|
logPath,
|
|
1427
1527
|
};
|
|
1428
1528
|
store.addProcess(id, proc);
|
|
1529
|
+
audit({ event: "sandbox.process.start", sandboxId: id, driver: record.driver ?? driver.name, attributes: { command: redactCommandForAudit(command), cwd: opts.cwd ?? "", procId } });
|
|
1429
1530
|
return sendJson(res, 201, proc);
|
|
1430
1531
|
}
|
|
1431
1532
|
catch (err) {
|
|
@@ -1530,6 +1631,7 @@ async function exposePort(res, { config, driver, store, capacity }, id, body) {
|
|
|
1530
1631
|
url: previewUrl(config, id, port),
|
|
1531
1632
|
};
|
|
1532
1633
|
store.addExposed(id, exposed);
|
|
1634
|
+
audit({ event: "sandbox.preview.expose", sandboxId: id, driver: store.get(id)?.driver ?? driver.name, attributes: { port, tokenConfigured: exposed.token !== null } });
|
|
1533
1635
|
return sendJson(res, 201, exposed);
|
|
1534
1636
|
}
|
|
1535
1637
|
function unexposePort(res, { store }, id, port) {
|
|
@@ -1538,6 +1640,7 @@ function unexposePort(res, { store }, id, port) {
|
|
|
1538
1640
|
const removed = store.removeExposed(id, port);
|
|
1539
1641
|
if (!removed)
|
|
1540
1642
|
return sendJson(res, 404, { error: "port not exposed" });
|
|
1643
|
+
audit({ event: "sandbox.preview.unexpose", sandboxId: id, driver: store.get(id)?.driver, attributes: { port } });
|
|
1541
1644
|
return sendJson(res, 200, { ok: true });
|
|
1542
1645
|
}
|
|
1543
1646
|
function createSession(res, { store }, id, body) {
|