@botlearn-course/daemon 0.0.20-beta.1 → 0.0.20-beta.2

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.
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { chmodSync, closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, rmSync, writeSync, } from "node:fs";
2
+ import { chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, rmSync, writeSync, } from "node:fs";
3
3
  import path from "node:path";
4
+ import { isWorkspaceQuiescenceProof, } from "./runtime-quiescence.js";
4
5
  import { defaultCaseFold, validateWorkspaceEntrySet, WORKSPACE_ENTRY_SET_SCHEMA, } from "./workspace-entry-set.js";
5
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;
6
7
  const EXCLUDED_NAMES = new Set(["node_modules"]);
@@ -107,30 +108,52 @@ function fsyncDirectory(directory) {
107
108
  closeSync(handle);
108
109
  }
109
110
  }
111
+ function descriptorPath(handle, fallback) {
112
+ if (existsSync("/proc/self/fd")) {
113
+ const value = path.join("/proc/self/fd", String(handle));
114
+ if (!existsSync(value))
115
+ fail("workspace_snapshot_fd_traversal_unavailable");
116
+ return value;
117
+ }
118
+ // Darwin has no procfs open-file path that supports relative child lookup. It is used
119
+ // only by local tests/BYOA; managed durable sandboxes are Linux and fail closed above.
120
+ return fallback;
121
+ }
110
122
  function stageOnce(options, paths) {
111
123
  const stagingDirectory = path.join(paths.checkpointRoot, randomUUID());
112
124
  mkdirSync(stagingDirectory, { mode: 0o700 });
113
125
  chmodSync(stagingDirectory, 0o700);
114
126
  try {
115
- const rootStats = lstatSync(paths.workspace, { bigint: true });
116
- if (!rootStats.isDirectory())
127
+ const rootHandle = openSync(paths.workspace, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
128
+ const rootStats = fstatSync(rootHandle, { bigint: true });
129
+ if (!rootStats.isDirectory()) {
130
+ closeSync(rootHandle);
117
131
  fail("workspace_snapshot_root_invalid");
132
+ }
118
133
  const entries = [];
119
134
  const files = [];
120
135
  let fileCount = 0;
121
136
  let totalBytes = 0;
122
- function walk(sourceDirectory, destinationDirectory, segments) {
123
- const directoryBefore = lstatSync(sourceDirectory, { bigint: true });
137
+ let estimatedEntrySetBytes = Buffer.byteLength('{"schemaVersion":"agent-workspace-entry-set/1","entries":[]}', "utf8");
138
+ function appendEntry(entry) {
139
+ estimatedEntrySetBytes += Buffer.byteLength(JSON.stringify(entry), "utf8") + 1;
140
+ if (estimatedEntrySetBytes > options.limits.maxEntrySetBytes) {
141
+ fail("workspace_snapshot_limit_exceeded");
142
+ }
143
+ entries.push(entry);
144
+ }
145
+ function walk(sourceHandle, sourceFallback, destinationDirectory, segments) {
146
+ const directoryBefore = fstatSync(sourceHandle, { bigint: true });
124
147
  assertSourceNode(directoryBefore, {
125
148
  rootDevice: rootStats.dev,
126
149
  expected: "directory",
127
150
  });
151
+ const sourceDirectory = descriptorPath(sourceHandle, sourceFallback);
128
152
  const children = readdirSync(sourceDirectory, {
129
- withFileTypes: true,
130
153
  encoding: "buffer",
131
- }).sort((left, right) => Buffer.compare(left.name, right.name));
154
+ }).sort((left, right) => Buffer.compare(left, right));
132
155
  for (const child of children) {
133
- const name = decodeName(child.name);
156
+ const name = decodeName(child);
134
157
  if (excludedName(name))
135
158
  continue;
136
159
  const relativeSegments = [...segments, name];
@@ -145,13 +168,21 @@ function stageOnce(options, paths) {
145
168
  fail("workspace_snapshot_unsupported_file_type");
146
169
  if (stats.isDirectory()) {
147
170
  assertSourceNode(stats, { rootDevice: rootStats.dev, expected: "directory" });
171
+ const childHandle = openSync(source, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
172
+ const openedDirectory = fstatSync(childHandle, { bigint: true });
173
+ if (!sameIdentity(stats, openedDirectory)) {
174
+ closeSync(childHandle);
175
+ fail("workspace_snapshot_source_changed", true);
176
+ }
148
177
  mkdirSync(destination, { mode: 0o700 });
149
178
  chmodSync(destination, 0o700);
150
- entries.push({ type: "directory", path: relativePath, mode: "0700" });
151
- if (entries.length > options.limits.maxEntrySetBytes) {
152
- fail("workspace_snapshot_limit_exceeded");
179
+ appendEntry({ type: "directory", path: relativePath, mode: "0700" });
180
+ try {
181
+ walk(childHandle, source, destination, relativeSegments);
182
+ }
183
+ finally {
184
+ closeSync(childHandle);
153
185
  }
154
- walk(source, destination, relativeSegments);
155
186
  continue;
156
187
  }
157
188
  const copied = copyStableFile(source, destination, {
@@ -163,7 +194,7 @@ function stageOnce(options, paths) {
163
194
  if (fileCount > options.limits.maxFileCount ||
164
195
  totalBytes > options.limits.maxTotalBytes)
165
196
  fail("workspace_snapshot_limit_exceeded");
166
- entries.push({
197
+ appendEntry({
167
198
  type: "file",
168
199
  path: relativePath,
169
200
  mode: copied.mode,
@@ -176,17 +207,19 @@ function stageOnce(options, paths) {
176
207
  sizeBytes: copied.sizeBytes,
177
208
  sha256: copied.sha256,
178
209
  });
179
- if (entries.length > options.limits.maxEntrySetBytes) {
180
- fail("workspace_snapshot_limit_exceeded");
181
- }
182
210
  }
183
- const directoryAfter = lstatSync(sourceDirectory, { bigint: true });
211
+ const directoryAfter = fstatSync(sourceHandle, { bigint: true });
184
212
  if (!sameIdentity(directoryBefore, directoryAfter)) {
185
213
  fail("workspace_snapshot_source_changed", true);
186
214
  }
187
215
  fsyncDirectory(destinationDirectory);
188
216
  }
189
- walk(paths.workspace, stagingDirectory, []);
217
+ try {
218
+ walk(rootHandle, paths.workspace, stagingDirectory, []);
219
+ }
220
+ finally {
221
+ closeSync(rootHandle);
222
+ }
190
223
  entries.sort((left, right) => Buffer.compare(Buffer.from(left.path, "utf8"), Buffer.from(right.path, "utf8")));
191
224
  files.sort((left, right) => Buffer.compare(Buffer.from(left.path, "utf8"), Buffer.from(right.path, "utf8")));
192
225
  const entrySet = validateWorkspaceEntrySet({ schemaVersion: WORKSPACE_ENTRY_SET_SCHEMA, entries }, options.limits);
@@ -200,7 +233,8 @@ function stageOnce(options, paths) {
200
233
  export function stageWorkspaceSnapshot(options) {
201
234
  if (!UUID.test(options.checkpointId))
202
235
  fail("workspace_snapshot_checkpoint_invalid");
203
- if (options.quiescenceProof.proofVersion !== "workspace-quiescence-proof/1" ||
236
+ if (!isWorkspaceQuiescenceProof(options.quiescenceProof) ||
237
+ options.quiescenceProof.proofVersion !== "workspace-quiescence-proof/1" ||
204
238
  !UUID.test(options.quiescenceProof.sandboxId) ||
205
239
  !UUID.test(options.quiescenceProof.runtimeSessionId) ||
206
240
  options.quiescenceProof.checkpointId !== options.checkpointId ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botlearn-course/daemon",
3
- "version": "0.0.20-beta.1",
3
+ "version": "0.0.20-beta.2",
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": {