@botlearn-course/daemon 0.0.20-beta.9 → 0.0.21-beta.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botlearn-course/daemon",
3
- "version": "0.0.20-beta.9",
3
+ "version": "0.0.21-beta.1",
4
4
  "description": "Lightweight BotLearn Course daemon: run course tasks on your own machine with your own agent runtime (BYOA).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,16 +0,0 @@
1
- export declare const WORKSPACE_MATERIALIZATION_SCHEMA: "agent-workspace-materialization/1";
2
- export interface WorkspaceMaterializationMarker {
3
- schemaVersion: typeof WORKSPACE_MATERIALIZATION_SCHEMA;
4
- sandboxId: string;
5
- sandboxGeneration: number;
6
- runtimeSessionId: string;
7
- workspaceContinuityState: "healthy" | "degraded";
8
- snapshotId: string | null;
9
- revision: number;
10
- contentSha256: string | null;
11
- writtenAt: string;
12
- }
13
- export declare function workspaceMaterializationMarkerPath(sandboxId: string, runtimeSessionId: string): string;
14
- export declare function writeWorkspaceMaterializationMarker(marker: WorkspaceMaterializationMarker): void;
15
- export declare function readWorkspaceMaterializationMarker(sandboxId: string, runtimeSessionId: string): WorkspaceMaterializationMarker | null;
16
- export declare function removeWorkspaceMaterializationMarker(sandboxId: string, runtimeSessionId: string): boolean;
@@ -1,136 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
- import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
3
- import path from "node:path";
4
- import { ensureDaemonHome } from "./auth-store.js";
5
- export const WORKSPACE_MATERIALIZATION_SCHEMA = "agent-workspace-materialization/1";
6
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
- const SHA256 = /^[0-9a-f]{64}$/;
8
- function assertUuid(value, label) {
9
- if (!UUID.test(value))
10
- throw new Error(label + " must be a UUID");
11
- }
12
- function markerDirectory(sandboxId) {
13
- assertUuid(sandboxId, "sandboxId");
14
- return path.join(ensureDaemonHome(), "agent-service-sandboxes", sandboxId, "workspace-materializations");
15
- }
16
- export function workspaceMaterializationMarkerPath(sandboxId, runtimeSessionId) {
17
- assertUuid(runtimeSessionId, "runtimeSessionId");
18
- return path.join(markerDirectory(sandboxId), runtimeSessionId + ".json");
19
- }
20
- function validateMarker(raw) {
21
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
22
- throw new Error("workspace materialization marker must be an object");
23
- }
24
- const marker = raw;
25
- const expectedKeys = [
26
- "schemaVersion",
27
- "sandboxId",
28
- "sandboxGeneration",
29
- "runtimeSessionId",
30
- "workspaceContinuityState",
31
- "snapshotId",
32
- "revision",
33
- "contentSha256",
34
- "writtenAt",
35
- ].sort();
36
- if (Object.keys(marker).sort().some((key, index) => key !== expectedKeys[index]) ||
37
- Object.keys(marker).length !== expectedKeys.length)
38
- throw new Error("workspace materialization marker fields are invalid");
39
- if (marker.schemaVersion !== WORKSPACE_MATERIALIZATION_SCHEMA) {
40
- throw new Error("workspace materialization marker schema is unsupported");
41
- }
42
- if (typeof marker.sandboxId !== "string")
43
- throw new Error("sandboxId is required");
44
- if (typeof marker.runtimeSessionId !== "string") {
45
- throw new Error("runtimeSessionId is required");
46
- }
47
- assertUuid(marker.sandboxId, "sandboxId");
48
- assertUuid(marker.runtimeSessionId, "runtimeSessionId");
49
- if (!Number.isSafeInteger(marker.sandboxGeneration) ||
50
- marker.sandboxGeneration < 1 ||
51
- !Number.isSafeInteger(marker.revision) ||
52
- marker.revision < 0 ||
53
- typeof marker.writtenAt !== "string" ||
54
- Number.isNaN(Date.parse(marker.writtenAt)))
55
- throw new Error("workspace materialization marker values are invalid");
56
- if (marker.workspaceContinuityState !== "healthy" &&
57
- marker.workspaceContinuityState !== "degraded")
58
- throw new Error("workspace materialization continuity is invalid");
59
- if (marker.snapshotId !== null) {
60
- if (typeof marker.snapshotId !== "string")
61
- throw new Error("snapshotId is invalid");
62
- assertUuid(marker.snapshotId, "snapshotId");
63
- }
64
- const revisionZero = marker.revision === 0;
65
- if (revisionZero !== (marker.snapshotId === null) ||
66
- revisionZero !== (marker.contentSha256 === null) ||
67
- (!revisionZero &&
68
- (typeof marker.contentSha256 !== "string" || !SHA256.test(marker.contentSha256)))) {
69
- throw new Error("workspace materialization marker pointer is inconsistent");
70
- }
71
- return marker;
72
- }
73
- export function writeWorkspaceMaterializationMarker(marker) {
74
- validateMarker(marker);
75
- const directory = markerDirectory(marker.sandboxId);
76
- mkdirSync(directory, { recursive: true, mode: 0o700 });
77
- try {
78
- chmodSync(directory, 0o700);
79
- }
80
- catch {
81
- // Windows best effort.
82
- }
83
- const target = workspaceMaterializationMarkerPath(marker.sandboxId, marker.runtimeSessionId);
84
- const staging = target + ".tmp-" + process.pid + "-" + randomBytes(4).toString("hex");
85
- try {
86
- writeFileSync(staging, JSON.stringify(marker), { encoding: "utf8", mode: 0o600 });
87
- const file = openSync(staging, "r");
88
- try {
89
- fsyncSync(file);
90
- }
91
- finally {
92
- closeSync(file);
93
- }
94
- renameSync(staging, target);
95
- try {
96
- chmodSync(target, 0o600);
97
- }
98
- catch {
99
- // Windows best effort.
100
- }
101
- try {
102
- const parent = openSync(directory, "r");
103
- try {
104
- fsyncSync(parent);
105
- }
106
- finally {
107
- closeSync(parent);
108
- }
109
- }
110
- catch {
111
- // Some platforms cannot fsync a directory.
112
- }
113
- }
114
- catch (error) {
115
- try {
116
- unlinkSync(staging);
117
- }
118
- catch {
119
- // Staging may not have been created.
120
- }
121
- throw error;
122
- }
123
- }
124
- export function readWorkspaceMaterializationMarker(sandboxId, runtimeSessionId) {
125
- const file = workspaceMaterializationMarkerPath(sandboxId, runtimeSessionId);
126
- if (!existsSync(file))
127
- return null;
128
- return validateMarker(JSON.parse(readFileSync(file, "utf8")));
129
- }
130
- export function removeWorkspaceMaterializationMarker(sandboxId, runtimeSessionId) {
131
- const file = workspaceMaterializationMarkerPath(sandboxId, runtimeSessionId);
132
- if (!existsSync(file))
133
- return false;
134
- unlinkSync(file);
135
- return true;
136
- }
@@ -1,4 +0,0 @@
1
- export declare const RUNTIME_WORKSPACE_QUOTA_BYTES: number;
2
- export declare const RUNTIME_WORKSPACE_INODE_QUOTA = 2000;
3
- export declare function assertRuntimeWorkspaceQuota(workspaceRoot: string): void;
4
- export declare function applyRuntimeWorkspaceQuota(runtimeSessionId: string, sandboxGeneration: number): Promise<void>;
@@ -1,42 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { readFileSync, statfsSync, statSync } from "node:fs";
3
- import { promisify } from "node:util";
4
- export const RUNTIME_WORKSPACE_QUOTA_BYTES = 512 * 1024 * 1024;
5
- export const RUNTIME_WORKSPACE_INODE_QUOTA = 2_000;
6
- const WORKSPACE_BACKING_BYTES = 3 * 1024 * 1024 * 1024;
7
- const QUOTA_MARKER = "/var/lib/botlearn-runtime/workspace-quota.ready";
8
- const RUNTIME_LAUNCHER = "/opt/botlearn/bin/botlearn-runtime-launcher";
9
- const execFileAsync = promisify(execFile);
10
- export function assertRuntimeWorkspaceQuota(workspaceRoot) {
11
- if (process.env.BOTLEARN_DAEMON_ENABLE_FAKE_RUNTIME === "1" || process.env.NODE_ENV === "test") {
12
- return;
13
- }
14
- if (process.env.BOTLEARN_RUNTIME_WORKSPACE_QUOTA_BYTES !== String(RUNTIME_WORKSPACE_QUOTA_BYTES) ||
15
- process.env.BOTLEARN_RUNTIME_WORKSPACE_INODE_QUOTA !== String(RUNTIME_WORKSPACE_INODE_QUOTA)) {
16
- throw new Error("workspace_runtime_quota_unverified");
17
- }
18
- const filesystem = statfsSync(workspaceRoot, { bigint: true });
19
- const marker = statSync(QUOTA_MARKER);
20
- if (filesystem.blocks * filesystem.bsize < BigInt(WORKSPACE_BACKING_BYTES) ||
21
- marker.uid !== 0 || (marker.mode & 0o777) !== 0o600 ||
22
- readFileSync(QUOTA_MARKER, "utf8") !== "workspace-filesystem-quota/1\n") {
23
- throw new Error("workspace_runtime_quota_probe_failed");
24
- }
25
- }
26
- export async function applyRuntimeWorkspaceQuota(runtimeSessionId, sandboxGeneration) {
27
- if (process.env.BOTLEARN_DAEMON_ENABLE_FAKE_RUNTIME === "1" || process.env.NODE_ENV === "test") {
28
- return;
29
- }
30
- const args = ["--workspace-quota-apply", runtimeSessionId, String(sandboxGeneration)];
31
- if (process.env.BOTLEARN_RUNTIME_LAUNCH_MODE === "sudo") {
32
- await execFileAsync("/usr/bin/sudo", ["-n", "--", RUNTIME_LAUNCHER, ...args], {
33
- timeout: 10_000,
34
- });
35
- return;
36
- }
37
- if (process.env.BOTLEARN_RUNTIME_LAUNCH_MODE === "direct-uid") {
38
- await execFileAsync(RUNTIME_LAUNCHER, args, { timeout: 10_000 });
39
- return;
40
- }
41
- throw new Error("workspace_runtime_quota_helper_unavailable");
42
- }
@@ -1,42 +0,0 @@
1
- import { type WorkspaceEntry, type WorkspaceEntrySetLimits } from "./workspace-entry-set.js";
2
- export declare class WorkspaceRestoreError extends Error {
3
- readonly code: string;
4
- readonly retryable: boolean;
5
- constructor(code: string, retryable?: boolean);
6
- }
7
- export interface WorkspaceRestoreDescriptor {
8
- sandboxId: string;
9
- sandboxGeneration: number;
10
- runtimeSessionId: string;
11
- continuityState: "healthy" | "degraded";
12
- continuityErrorCode: string | null;
13
- snapshotId: string | null;
14
- revision: number;
15
- contentSha256: string | null;
16
- expectedFileCount: number;
17
- expectedEntryCount: number;
18
- expectedTotalBytes: number;
19
- }
20
- export interface WorkspaceRestoreOptions {
21
- workspaceDirectory: string;
22
- controlDirectory: string;
23
- descriptor: WorkspaceRestoreDescriptor;
24
- entrySet: unknown;
25
- limits: WorkspaceEntrySetLimits;
26
- requiredFreeBytes: number;
27
- downloadFile(entry: WorkspaceEntry, partPath: string): Promise<void>;
28
- }
29
- export interface WorkspaceRestoreCapacityOptions {
30
- workspaceDirectory: string;
31
- descriptor: WorkspaceRestoreDescriptor;
32
- requiredFreeBytes: number;
33
- entryCopies?: 1 | 2;
34
- }
35
- export declare function workspaceRestoreJournalPath(controlDirectory: string, runtimeSessionId: string): string;
36
- export declare function recoverWorkspaceRestoreSwap(options: {
37
- workspaceDirectory: string;
38
- controlDirectory: string;
39
- descriptor: WorkspaceRestoreDescriptor;
40
- }): boolean;
41
- export declare function restoreWorkspaceSnapshot(options: WorkspaceRestoreOptions): Promise<void>;
42
- export declare function assertWorkspaceRestoreCapacity(options: WorkspaceRestoreCapacityOptions): void;
@@ -1,347 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statfsSync, unlinkSync, writeFileSync, } from "node:fs";
3
- import path from "node:path";
4
- import { validateWorkspaceEntrySet, } from "./workspace-entry-set.js";
5
- import { WORKSPACE_MATERIALIZATION_SCHEMA, writeWorkspaceMaterializationMarker, } from "./workspace-materialization.js";
6
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
- const SHA256 = /^[0-9a-f]{64}$/;
8
- const RESTORE_JOURNAL_SCHEMA = "agent-workspace-restore-journal/1";
9
- const COPY_CHUNK_BYTES = 64 * 1024;
10
- export class WorkspaceRestoreError extends Error {
11
- code;
12
- retryable;
13
- constructor(code, retryable = false) {
14
- super(code);
15
- this.code = code;
16
- this.retryable = retryable;
17
- this.name = "WorkspaceRestoreError";
18
- }
19
- }
20
- function fail(code) {
21
- throw new WorkspaceRestoreError(code, code === "workspace_restore_unavailable" ||
22
- code === "workspace_restore_disk_pressure" ||
23
- code === "workspace_restore_inode_pressure");
24
- }
25
- function assertUuid(value, label) {
26
- if (!UUID.test(value))
27
- fail(`workspace_restore_${label}_invalid`);
28
- }
29
- function hasExactKeys(value, expected) {
30
- const actual = Object.keys(value).sort();
31
- const sortedExpected = [...expected].sort();
32
- return actual.length === sortedExpected.length &&
33
- actual.every((key, index) => key === sortedExpected[index]);
34
- }
35
- function containsPath(parent, candidate) {
36
- const relative = path.relative(parent, candidate);
37
- return relative === "" || (!relative.startsWith(".." + path.sep) && relative !== "..");
38
- }
39
- function validJournalSwapName(value, prefix) {
40
- return typeof value === "string" && value.startsWith(prefix) &&
41
- UUID.test(value.slice(prefix.length));
42
- }
43
- function assertControlPath(workspaceDirectory, controlDirectory) {
44
- if (containsPath(path.resolve(workspaceDirectory), path.resolve(controlDirectory))) {
45
- fail("workspace_restore_control_path_invalid");
46
- }
47
- }
48
- function assertDescriptor(descriptor) {
49
- assertUuid(descriptor.sandboxId, "sandbox_id");
50
- assertUuid(descriptor.runtimeSessionId, "session_id");
51
- if (!Number.isSafeInteger(descriptor.sandboxGeneration) ||
52
- descriptor.sandboxGeneration < 1 ||
53
- !Number.isSafeInteger(descriptor.revision) ||
54
- descriptor.revision < 0 ||
55
- !Number.isSafeInteger(descriptor.expectedFileCount) ||
56
- descriptor.expectedFileCount < 0 ||
57
- !Number.isSafeInteger(descriptor.expectedEntryCount) ||
58
- descriptor.expectedEntryCount < 0 ||
59
- !Number.isSafeInteger(descriptor.expectedTotalBytes) ||
60
- descriptor.expectedTotalBytes < 0 ||
61
- (descriptor.continuityState !== "healthy" && descriptor.continuityState !== "degraded") ||
62
- (descriptor.continuityState === "degraded" && !descriptor.continuityErrorCode) ||
63
- (descriptor.continuityState === "healthy" && descriptor.continuityErrorCode !== null))
64
- fail("workspace_restore_descriptor_invalid");
65
- const revisionZero = descriptor.revision === 0;
66
- if (revisionZero !== (descriptor.snapshotId === null) ||
67
- revisionZero !== (descriptor.contentSha256 === null) ||
68
- (!revisionZero &&
69
- (!UUID.test(descriptor.snapshotId) || !SHA256.test(descriptor.contentSha256))) ||
70
- (revisionZero &&
71
- (descriptor.expectedFileCount !== 0 || descriptor.expectedEntryCount !== 0 ||
72
- descriptor.expectedTotalBytes !== 0)))
73
- fail("workspace_restore_descriptor_invalid");
74
- }
75
- export function workspaceRestoreJournalPath(controlDirectory, runtimeSessionId) {
76
- assertUuid(runtimeSessionId, "session_id");
77
- return path.join(controlDirectory, "workspace-restore-journals", `${runtimeSessionId}.json`);
78
- }
79
- function writeJournal(file, journal) {
80
- const directory = path.dirname(file);
81
- mkdirSync(directory, { recursive: true, mode: 0o700 });
82
- chmodSync(directory, 0o700);
83
- const staging = `${file}.tmp-${process.pid}-${randomUUID()}`;
84
- try {
85
- writeFileSync(staging, JSON.stringify(journal), { encoding: "utf8", mode: 0o600 });
86
- const handle = openSync(staging, constants.O_RDONLY | constants.O_NOFOLLOW);
87
- try {
88
- fsyncSync(handle);
89
- }
90
- finally {
91
- closeSync(handle);
92
- }
93
- renameSync(staging, file);
94
- fsyncDirectory(directory);
95
- }
96
- catch (error) {
97
- try {
98
- unlinkSync(staging);
99
- }
100
- catch {
101
- // The atomic staging file may not have been created.
102
- }
103
- throw error;
104
- }
105
- }
106
- function parseJournal(raw, descriptor) {
107
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
108
- fail("workspace_restore_journal_invalid");
109
- }
110
- const value = raw;
111
- if (!hasExactKeys(value, [
112
- "schemaVersion",
113
- "sandboxId",
114
- "sandboxGeneration",
115
- "runtimeSessionId",
116
- "stagingName",
117
- "backupName",
118
- "hadWorkspace",
119
- "phase",
120
- "marker",
121
- ]))
122
- fail("workspace_restore_journal_invalid");
123
- const namePrefix = `.botlearn-restore-${descriptor.runtimeSessionId}-`;
124
- const backupPrefix = `.botlearn-backup-${descriptor.runtimeSessionId}-`;
125
- if (value.schemaVersion !== RESTORE_JOURNAL_SCHEMA ||
126
- value.sandboxId !== descriptor.sandboxId ||
127
- value.sandboxGeneration !== descriptor.sandboxGeneration ||
128
- value.runtimeSessionId !== descriptor.runtimeSessionId ||
129
- !validJournalSwapName(value.stagingName, namePrefix) ||
130
- !validJournalSwapName(value.backupName, backupPrefix) ||
131
- typeof value.hadWorkspace !== "boolean" ||
132
- !["prepared", "old_moved", "new_moved"].includes(String(value.phase)))
133
- fail("workspace_restore_journal_invalid");
134
- const marker = value.marker;
135
- if (!marker || marker.schemaVersion !== WORKSPACE_MATERIALIZATION_SCHEMA ||
136
- marker.sandboxId !== descriptor.sandboxId ||
137
- marker.sandboxGeneration !== descriptor.sandboxGeneration ||
138
- marker.runtimeSessionId !== descriptor.runtimeSessionId ||
139
- marker.workspaceContinuityState !== descriptor.continuityState ||
140
- marker.snapshotId !== descriptor.snapshotId || marker.revision !== descriptor.revision ||
141
- marker.contentSha256 !== descriptor.contentSha256)
142
- fail("workspace_restore_journal_invalid");
143
- return value;
144
- }
145
- function fsyncDirectory(directory) {
146
- const handle = openSync(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
147
- try {
148
- fsyncSync(handle);
149
- }
150
- finally {
151
- closeSync(handle);
152
- }
153
- }
154
- function verifyDownloadedFile(file, expected) {
155
- const handle = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
156
- try {
157
- const stats = fstatSync(handle, { bigint: true });
158
- if (!stats.isFile() || stats.nlink !== 1n || stats.size !== BigInt(expected.expectedSize)) {
159
- fail("workspace_restore_integrity_failed");
160
- }
161
- const hash = createHash("sha256");
162
- const buffer = Buffer.allocUnsafe(COPY_CHUNK_BYTES);
163
- let total = 0;
164
- while (true) {
165
- const bytesRead = readSync(handle, buffer, 0, buffer.length, null);
166
- if (bytesRead === 0)
167
- break;
168
- total += bytesRead;
169
- hash.update(buffer.subarray(0, bytesRead));
170
- }
171
- if (total !== expected.expectedSize || hash.digest("hex") !== expected.expectedSha256) {
172
- fail("workspace_restore_integrity_failed");
173
- }
174
- fsyncSync(handle);
175
- }
176
- finally {
177
- closeSync(handle);
178
- }
179
- }
180
- function markerFromDescriptor(descriptor) {
181
- return {
182
- schemaVersion: WORKSPACE_MATERIALIZATION_SCHEMA,
183
- sandboxId: descriptor.sandboxId,
184
- sandboxGeneration: descriptor.sandboxGeneration,
185
- runtimeSessionId: descriptor.runtimeSessionId,
186
- workspaceContinuityState: descriptor.continuityState,
187
- snapshotId: descriptor.snapshotId,
188
- revision: descriptor.revision,
189
- contentSha256: descriptor.contentSha256,
190
- writtenAt: new Date().toISOString(),
191
- };
192
- }
193
- export function recoverWorkspaceRestoreSwap(options) {
194
- assertDescriptor(options.descriptor);
195
- assertControlPath(options.workspaceDirectory, options.controlDirectory);
196
- const journalFile = workspaceRestoreJournalPath(options.controlDirectory, options.descriptor.runtimeSessionId);
197
- if (!existsSync(journalFile))
198
- return false;
199
- const journal = parseJournal(JSON.parse(readFileSync(journalFile, "utf8")), options.descriptor);
200
- const parent = path.dirname(options.workspaceDirectory);
201
- const staging = path.join(parent, journal.stagingName);
202
- const backup = path.join(parent, journal.backupName);
203
- const workspaceExists = existsSync(options.workspaceDirectory);
204
- const stagingExists = existsSync(staging);
205
- const backupExists = existsSync(backup);
206
- if (journal.phase === "new_moved" ||
207
- (journal.phase === "old_moved" && workspaceExists && !stagingExists)) {
208
- if (!workspaceExists)
209
- fail("workspace_restore_journal_invalid");
210
- writeWorkspaceMaterializationMarker(journal.marker);
211
- if (backupExists)
212
- rmSync(backup, { recursive: true, force: true });
213
- unlinkSync(journalFile);
214
- fsyncDirectory(path.dirname(journalFile));
215
- fsyncDirectory(parent);
216
- return true;
217
- }
218
- if (!stagingExists)
219
- fail("workspace_restore_journal_invalid");
220
- if (!workspaceExists && backupExists)
221
- renameSync(backup, options.workspaceDirectory);
222
- else if (!workspaceExists && journal.hadWorkspace)
223
- fail("workspace_restore_journal_invalid");
224
- else if (workspaceExists && backupExists)
225
- fail("workspace_restore_journal_invalid");
226
- rmSync(staging, { recursive: true, force: true });
227
- unlinkSync(journalFile);
228
- fsyncDirectory(path.dirname(journalFile));
229
- fsyncDirectory(parent);
230
- return true;
231
- }
232
- export async function restoreWorkspaceSnapshot(options) {
233
- const descriptor = options.descriptor;
234
- assertDescriptor(descriptor);
235
- assertControlPath(options.workspaceDirectory, options.controlDirectory);
236
- assertWorkspaceRestoreCapacity(options);
237
- recoverWorkspaceRestoreSwap({
238
- workspaceDirectory: options.workspaceDirectory,
239
- controlDirectory: options.controlDirectory,
240
- descriptor,
241
- });
242
- const canonical = validateWorkspaceEntrySet(options.entrySet, options.limits);
243
- if (canonical.entries.length !== descriptor.expectedEntryCount ||
244
- canonical.fileCount !== descriptor.expectedFileCount ||
245
- canonical.totalBytes !== descriptor.expectedTotalBytes ||
246
- (descriptor.revision > 0 && canonical.contentSha256 !== descriptor.contentSha256))
247
- fail("workspace_restore_integrity_failed");
248
- const parent = path.dirname(options.workspaceDirectory);
249
- const nonce = randomUUID();
250
- const stagingName = `.botlearn-restore-${descriptor.runtimeSessionId}-${nonce}`;
251
- const backupName = `.botlearn-backup-${descriptor.runtimeSessionId}-${nonce}`;
252
- const staging = path.join(parent, stagingName);
253
- const backup = path.join(parent, backupName);
254
- mkdirSync(staging, { mode: 0o700 });
255
- chmodSync(staging, 0o700);
256
- let journalWritten = false;
257
- try {
258
- for (const entry of canonical.entries) {
259
- const destination = path.join(staging, ...entry.path.split("/"));
260
- if (entry.type === "directory") {
261
- mkdirSync(destination, { recursive: true, mode: 0o700 });
262
- chmodSync(destination, 0o700);
263
- continue;
264
- }
265
- mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
266
- const part = `${destination}.part`;
267
- await options.downloadFile(entry, part);
268
- verifyDownloadedFile(part, {
269
- expectedSize: entry.sizeBytes,
270
- expectedSha256: entry.sha256,
271
- });
272
- chmodSync(part, entry.mode === "0700" ? 0o700 : 0o600);
273
- renameSync(part, destination);
274
- }
275
- const directories = new Set([staging]);
276
- for (const entry of canonical.entries) {
277
- let directory = entry.type === "directory"
278
- ? path.join(staging, ...entry.path.split("/"))
279
- : path.dirname(path.join(staging, ...entry.path.split("/")));
280
- while (directory.startsWith(staging)) {
281
- directories.add(directory);
282
- if (directory === staging)
283
- break;
284
- directory = path.dirname(directory);
285
- }
286
- }
287
- for (const directory of [...directories].sort((a, b) => b.length - a.length)) {
288
- fsyncDirectory(directory);
289
- }
290
- const journalFile = workspaceRestoreJournalPath(options.controlDirectory, descriptor.runtimeSessionId);
291
- const journal = {
292
- schemaVersion: RESTORE_JOURNAL_SCHEMA,
293
- sandboxId: descriptor.sandboxId,
294
- sandboxGeneration: descriptor.sandboxGeneration,
295
- runtimeSessionId: descriptor.runtimeSessionId,
296
- stagingName,
297
- backupName,
298
- hadWorkspace: existsSync(options.workspaceDirectory),
299
- phase: "prepared",
300
- marker: markerFromDescriptor(descriptor),
301
- };
302
- writeJournal(journalFile, journal);
303
- journalWritten = true;
304
- if (journal.hadWorkspace)
305
- renameSync(options.workspaceDirectory, backup);
306
- journal.phase = "old_moved";
307
- writeJournal(journalFile, journal);
308
- renameSync(staging, options.workspaceDirectory);
309
- fsyncDirectory(parent);
310
- journal.phase = "new_moved";
311
- writeJournal(journalFile, journal);
312
- writeWorkspaceMaterializationMarker(journal.marker);
313
- if (journal.hadWorkspace)
314
- rmSync(backup, { recursive: true, force: true });
315
- unlinkSync(journalFile);
316
- fsyncDirectory(path.dirname(journalFile));
317
- fsyncDirectory(parent);
318
- }
319
- catch (error) {
320
- if (!journalWritten)
321
- rmSync(staging, { recursive: true, force: true });
322
- throw error;
323
- }
324
- }
325
- export function assertWorkspaceRestoreCapacity(options) {
326
- const { descriptor } = options;
327
- assertDescriptor(descriptor);
328
- if (!Number.isSafeInteger(options.requiredFreeBytes) ||
329
- options.requiredFreeBytes < descriptor.expectedTotalBytes)
330
- fail("workspace_restore_reservation_invalid");
331
- const parent = path.dirname(options.workspaceDirectory);
332
- mkdirSync(parent, { recursive: true, mode: 0o700 });
333
- if (existsSync(options.workspaceDirectory)) {
334
- const current = lstatSync(options.workspaceDirectory, { bigint: true });
335
- if (!current.isDirectory() || current.isSymbolicLink()) {
336
- fail("workspace_restore_existing_workspace_invalid");
337
- }
338
- }
339
- const filesystem = statfsSync(parent, { bigint: true });
340
- if (filesystem.bavail * filesystem.bsize < BigInt(options.requiredFreeBytes)) {
341
- fail("workspace_restore_disk_pressure");
342
- }
343
- const entryCopies = options.entryCopies ?? 1;
344
- if (filesystem.ffree < BigInt(descriptor.expectedEntryCount * entryCopies + 16)) {
345
- fail("workspace_restore_inode_pressure");
346
- }
347
- }