@botlearn-course/daemon 0.0.20-beta.8 → 0.0.20

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.
@@ -20,9 +20,9 @@ export declare function runtimeProfileDir(agentRunId: string): string;
20
20
  export declare function runtimeProfileRunRootDir(agentRunId: string): string;
21
21
  export declare function runtimeSessionRootDir(runtimeSessionId: string, sandboxGeneration: number): string;
22
22
  /**
23
- * per-session workspace(ADR-015 §7):NAS managed sandbox 使用稳定路径
24
- * `<root>/sessions/<runtime_session_id>/workspace`;旧 provider 保留 generation 路径。
25
- * 两种路径都由 daemon session.open 时创建。
23
+ * per-session workspace(ADR-015 §7):managed sandbox 使用稳定路径
24
+ * `<root>/sessions/<runtime_session_id>/workspace`。该路径不含 generation
25
+ * 因为 NAS image generation 重新挂载后必须解析到同一个目录。
26
26
  */
27
27
  export declare function runtimeSessionWorkspaceDir(runtimeSessionId: string, sandboxGeneration: number): string;
28
28
  export declare function runtimeSessionTranscriptPath(runtimeSessionId: string, sandboxGeneration: number, agentRunId: string): string;
package/dist/workspace.js CHANGED
@@ -4,7 +4,6 @@ import { createHash, randomUUID } from "node:crypto";
4
4
  import path from "node:path";
5
5
  import { TextDecoder } from "node:util";
6
6
  import { daemonHome } from "./auth-store.js";
7
- import { nasWorkspaceEnabled } from "./workspace-filesystem.js";
8
7
  /**
9
8
  * 每 run 隔离工作区(spec §4):
10
9
  *
@@ -54,9 +53,9 @@ export function runtimeSessionRootDir(runtimeSessionId, sandboxGeneration) {
54
53
  return path.join(daemonHome(), "agent-service-sessions", runtimeSessionId, `generation-${sandboxGeneration}`);
55
54
  }
56
55
  /**
57
- * per-session workspace(ADR-015 §7):NAS managed sandbox 使用稳定路径
58
- * `<root>/sessions/<runtime_session_id>/workspace`;旧 provider 保留 generation 路径。
59
- * 两种路径都由 daemon session.open 时创建。
56
+ * per-session workspace(ADR-015 §7):managed sandbox 使用稳定路径
57
+ * `<root>/sessions/<runtime_session_id>/workspace`。该路径不含 generation
58
+ * 因为 NAS image generation 重新挂载后必须解析到同一个目录。
60
59
  */
61
60
  export function runtimeSessionWorkspaceDir(runtimeSessionId, sandboxGeneration) {
62
61
  assertSafeId(runtimeSessionId, "runtime_session_id");
@@ -65,10 +64,7 @@ export function runtimeSessionWorkspaceDir(runtimeSessionId, sandboxGeneration)
65
64
  }
66
65
  const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
67
66
  if (managedRoot) {
68
- if (nasWorkspaceEnabled()) {
69
- return path.join(managedRoot, "sessions", runtimeSessionId, "workspace");
70
- }
71
- return path.join(managedRoot, runtimeSessionId, `generation-${sandboxGeneration}`);
67
+ return path.join(managedRoot, "sessions", runtimeSessionId, "workspace");
72
68
  }
73
69
  return path.join(runtimeSessionRootDir(runtimeSessionId, sandboxGeneration), "workspace");
74
70
  }
@@ -77,9 +73,7 @@ function managedRuntimeSessionParentDir(runtimeSessionId) {
77
73
  const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
78
74
  if (!managedRoot)
79
75
  return null;
80
- return nasWorkspaceEnabled()
81
- ? path.join(managedRoot, "sessions", runtimeSessionId)
82
- : path.join(managedRoot, runtimeSessionId);
76
+ return path.join(managedRoot, "sessions", runtimeSessionId);
83
77
  }
84
78
  export function runtimeSessionTranscriptPath(runtimeSessionId, sandboxGeneration, agentRunId) {
85
79
  assertSafeId(agentRunId, "agent_run_id");
@@ -218,8 +212,7 @@ export function workspaceCopyV1Supported() {
218
212
  }
219
213
  const COPY_STAGING_SUFFIX = "[A-Za-z0-9][A-Za-z0-9_-]{0,127}-[0-9a-f]{8}-[0-9a-f]{4}-" +
220
214
  "[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
221
- const MANAGED_COPY_STAGING_PATTERN = new RegExp(`^generation-[1-9][0-9]*\\.workspace-copy-${COPY_STAGING_SUFFIX}$`);
222
- const LOCAL_COPY_STAGING_PATTERN = new RegExp(`^workspace\\.workspace-copy-${COPY_STAGING_SUFFIX}$`);
215
+ const COPY_STAGING_PATTERN = new RegExp(`^workspace\\.workspace-copy-${COPY_STAGING_SUFFIX}$`);
223
216
  async function childDirectories(root) {
224
217
  try {
225
218
  return (await fs.readdir(root, { withFileTypes: true }))
@@ -236,16 +229,13 @@ async function childDirectories(root) {
236
229
  export async function cleanupRuntimeSessionWorkspaceCopyStaging() {
237
230
  const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
238
231
  if (managedRoot) {
239
- const sessionRoot = nasWorkspaceEnabled()
240
- ? path.join(managedRoot, "sessions")
241
- : managedRoot;
232
+ const sessionRoot = path.join(managedRoot, "sessions");
242
233
  for (const sessionName of await childDirectories(sessionRoot)) {
243
234
  if (!SAFE_ID_PATTERN.test(sessionName))
244
235
  continue;
245
236
  const sessionParent = path.join(sessionRoot, sessionName);
246
237
  for (const name of await childDirectories(sessionParent)) {
247
- if (MANAGED_COPY_STAGING_PATTERN.test(name)
248
- || LOCAL_COPY_STAGING_PATTERN.test(name)) {
238
+ if (COPY_STAGING_PATTERN.test(name)) {
249
239
  await fs.rm(path.join(sessionParent, name), { recursive: true, force: true });
250
240
  }
251
241
  }
@@ -261,7 +251,7 @@ export async function cleanupRuntimeSessionWorkspaceCopyStaging() {
261
251
  continue;
262
252
  const generationParent = path.join(sessionParent, generationName);
263
253
  for (const name of await childDirectories(generationParent)) {
264
- if (LOCAL_COPY_STAGING_PATTERN.test(name)) {
254
+ if (COPY_STAGING_PATTERN.test(name)) {
265
255
  await fs.rm(path.join(generationParent, name), { recursive: true, force: true });
266
256
  }
267
257
  }
@@ -721,9 +711,7 @@ export function removeRuntimeSessionWorkspace(runtimeSessionId, sandboxGeneratio
721
711
  });
722
712
  const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
723
713
  if (managedRoot && !preserveManagedWorkspace) {
724
- const sessionRoot = nasWorkspaceEnabled()
725
- ? path.join(managedRoot, "sessions", runtimeSessionId)
726
- : path.join(managedRoot, runtimeSessionId);
714
+ const sessionRoot = path.join(managedRoot, "sessions", runtimeSessionId);
727
715
  rmSync(sessionRoot, { recursive: true, force: true });
728
716
  }
729
717
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botlearn-course/daemon",
3
- "version": "0.0.20-beta.8",
3
+ "version": "0.0.20",
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;