@botlearn-course/daemon 0.0.20-beta.9 → 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.
package/README.md CHANGED
@@ -36,22 +36,22 @@ npx --yes --package @botlearn-course/daemon@latest botlearn-course-daemon course
36
36
  | `course doctor` | 打印 Auth 状态(不含 token)与 runtime 探测表(安装路径 / 版本 / 登录状态 / 安装提示)。 |
37
37
  | `--help` / `--version` | 帮助与版本。 |
38
38
 
39
- 包内还包含供 BotLearn 托管 E2B Template 使用的内部命令
39
+ 包内还包含供 BotLearn 托管 sandbox 使用的内部命令
40
40
  `botlearn-sandbox-supervisor agent-service session`。它不是 BYOA 用户入口:生产环境只允许由
41
- Agent Service 以固定 argv 启动,通过 stdin 接收一次性 bootstrap。E2B 中 daemon/runtime 分别
41
+ Agent Service 以固定 argv 启动,通过 stdin 接收一次性 bootstrap。sandbox 中 daemon/runtime 分别
42
42
  降权到 `botlearn-control`/`user` UID;继承 `NoNewPrivs=1`、无法使用 sudo 的平台保留 root control
43
43
  daemon,由 root-owned 固定 launcher 清空附加组后直接降权为 `user`。两条路径都为 DeepSeek
44
44
  设置 `no_new_privs`,runtime 用户本身没有 sudo 权限。托管启动链只执行
45
- `/opt` 下的固定 Node、supervisor、daemon、launcher 与 DeepSeek 文件,不信任 E2B 会开放给 runtime
45
+ `/opt` 下的固定 Node、supervisor、daemon、launcher 与 DeepSeek 文件,不信任 provider 会开放给 runtime
46
46
  写入的 `/usr/local/bin`。
47
47
  `agent-service session --bootstrap-stdin` 同样属于受 supervisor 保护的内部协议,不应直接暴露给
48
48
  课程任务或 workspace shell。
49
49
 
50
50
  veFaaS Origin 的托管 workspace 使用用户级 NAS backing image。root bootstrap 完成 NFS、marker、
51
- ext4、loop mount 和固定 3 GiB 校验后,daemon 才发送 `sandbox.ready`。RuntimeSession 使用稳定路径
51
+ ext4、`fuse2fs` mount 和固定 3 GiB 校验后,daemon 才发送 `sandbox.ready`。RuntimeSession 使用稳定路径
52
52
  `/workspace/sessions/{runtime_session_id}/workspace`,Sandbox generation 只参与写入隔离。磁盘写满
53
53
  统一上报 `workspace_user_quota_exceeded`。pause 和 cleanup 进入 drain 后,daemon 先停止 runtime
54
- writer,再通过 root launcher 执行 ext4 sync、loop unmount、backing image 和 NFS parent fsync;完成后
54
+ writer,再通过 root launcher 执行 ext4 sync、`fuse2fs` unmount、backing image 和 NFS parent fsync;完成后
55
55
  才发送 `sandbox.drained` 并退出。
56
56
 
57
57
  ## 支持的 runtime
@@ -3,6 +3,13 @@ import type { WorkspaceScanMeasurement } from "./file-candidates.js";
3
3
  import { type RuntimeSkillProviderFactory } from "./runtime-skills.js";
4
4
  import { type PersistentSessionExecution, type PreparedPersistentTurn, type RunReportingClient } from "./run-dispatcher.js";
5
5
  import type { CourseRuntime, CourseRuntimeProfile, RunEvent, RunEventReceipt, RunFileCandidate, RunFileRecord, RunStartPayload } from "./types.js";
6
+ export declare function workspaceDurabilityCapabilities(): string[];
7
+ /**
8
+ * NAS 是运行时 workspace 的唯一持久事实来源,committed snapshot 只供前端查看。
9
+ * 任何 restore payload 都意味着对端仍在下发已删除的 S3 回写指令,必须 fail closed,
10
+ * 否则可能用落后的派生快照覆盖 NAS 中较新的内容。
11
+ */
12
+ export declare function assertWorkspaceRestoreAllowed(requestedRestore: unknown): void;
6
13
  export interface AgentServiceSandboxOptions {
7
14
  wsUrl: string;
8
15
  sandboxId: string;
@@ -1,7 +1,4 @@
1
- import { randomUUID } from "node:crypto";
2
- import { chmodSync, closeSync, createWriteStream, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
- import { Readable } from "node:stream";
4
- import { pipeline } from "node:stream/promises";
1
+ import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
5
2
  import path from "node:path";
6
3
  import { ensureDaemonHome } from "./auth-store.js";
7
4
  import { AGENT_SERVICE_WS_SCHEMA, AGENT_SERVICE_WS_SUBPROTOCOL, createSandboxFrame, createWorkspaceFileChunk, parseSandboxFrame, UnsupportedSandboxProtocolError, WORKSPACE_FILE_READ_BINARY_CAPABILITY, } from "./agent-service-ws-protocol.js";
@@ -16,12 +13,9 @@ import { readWorkspaceFile, WorkspaceFileReadError } from "./workspace-file-read
16
13
  import { checkpointWorkspace, WorkspaceSnapshotControlError, } from "./workspace-snapshot-control.js";
17
14
  import { assertWorkspaceWriterFreezeSupported, freezeRuntimeWriters, issueWorkspaceWriterFreezeProof, quiesceRuntimeWriters, thawRuntimeWriters, } from "./runtime-quiescence.js";
18
15
  import { WorkspaceSnapshotStagingError } from "./workspace-snapshot-staging.js";
19
- import { validateWorkspaceEntrySet, WorkspaceEntrySetError, } from "./workspace-entry-set.js";
20
- import { WORKSPACE_SNAPSHOT_POLICY_V1, workspaceSnapshotPolicyCapability, } from "./workspace-snapshot-policy.js";
21
- import { assertWorkspaceRestoreCapacity, restoreWorkspaceSnapshot, WorkspaceRestoreError, } from "./workspace-restore.js";
22
- import { readWorkspaceMaterializationMarker } from "./workspace-materialization.js";
23
- import { assertWorkspaceFilesystem, flushWorkspaceFilesystem, nasWorkspaceEnabled, } from "./workspace-filesystem.js";
24
- import { applyRuntimeWorkspaceQuota } from "./workspace-quota.js";
16
+ import { WorkspaceEntrySetError, } from "./workspace-entry-set.js";
17
+ import { workspaceSnapshotPolicyCapability, } from "./workspace-snapshot-policy.js";
18
+ import { assertWorkspaceFilesystem, flushWorkspaceFilesystem, } from "./workspace-filesystem.js";
25
19
  import { WebSocketClient, } from "./websocket-client.js";
26
20
  const RECONNECT_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000, 30_000];
27
21
  const MAX_SPOOL_FRAMES = 1024;
@@ -41,6 +35,23 @@ function agentServiceHttpBase(wsUrl) {
41
35
  }
42
36
  const RUNTIME_LOG_WINDOW_MS = 60_000;
43
37
  const WORKSPACE_FILE_TRANSFER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
38
+ export function workspaceDurabilityCapabilities() {
39
+ return [
40
+ "workspace_writer_freeze_v1",
41
+ "workspace_snapshot_v2",
42
+ "workspace_nas_v1",
43
+ ];
44
+ }
45
+ /**
46
+ * NAS 是运行时 workspace 的唯一持久事实来源,committed snapshot 只供前端查看。
47
+ * 任何 restore payload 都意味着对端仍在下发已删除的 S3 回写指令,必须 fail closed,
48
+ * 否则可能用落后的派生快照覆盖 NAS 中较新的内容。
49
+ */
50
+ export function assertWorkspaceRestoreAllowed(requestedRestore) {
51
+ if (requestedRestore !== null && requestedRestore !== undefined) {
52
+ throw new Error("workspace_restore_forbidden_for_nas");
53
+ }
54
+ }
44
55
  const DISABLED_RUNTIME_LOG_POLICY = {
45
56
  enabled: false,
46
57
  maxEventBytes: 4096,
@@ -836,7 +847,8 @@ export class AgentServiceSandboxClient {
836
847
  this.rejectPending(new Error("Agent Service sandbox generation changed"));
837
848
  for (const sessionId of Object.keys(this.state.sessions)) {
838
849
  revokeRuntimeSessionWorkspace(sessionId, this.state.sandboxGeneration);
839
- removeRuntimeSessionWorkspace(sessionId, this.state.sandboxGeneration, nasWorkspaceEnabled());
850
+ // NAS image 跨 generation 保留正文,只清掉本地 daemon 侧的 session 状态。
851
+ removeRuntimeSessionWorkspace(sessionId, this.state.sandboxGeneration, true);
840
852
  }
841
853
  this.state.sessions = {};
842
854
  this.state.sandboxGeneration = frame.sandbox_generation;
@@ -872,10 +884,7 @@ export class AgentServiceSandboxClient {
872
884
  protocol_versions: [AGENT_SERVICE_WS_SCHEMA],
873
885
  capabilities: [
874
886
  WORKSPACE_FILE_READ_BINARY_CAPABILITY,
875
- "workspace_writer_freeze_v1",
876
- "workspace_snapshot_v2",
877
- "workspace_restore_v1",
878
- ...(nasWorkspaceEnabled() ? ["workspace_nas_v1"] : []),
887
+ ...workspaceDurabilityCapabilities(),
879
888
  ...(workspaceCopyV1Supported() ? ["workspace_copy_v1"] : []),
880
889
  ],
881
890
  workspace_snapshot_policy: workspaceSnapshotPolicyCapability(),
@@ -1058,7 +1067,7 @@ export class AgentServiceSandboxClient {
1058
1067
  const courseRunId = frame.payload.course_run_id;
1059
1068
  const requestedWorkspaceRef = frame.payload.workspace_ref;
1060
1069
  const requestedCopy = frame.payload.workspace_copy;
1061
- const requestedRestore = frame.payload.workspace_restore;
1070
+ assertWorkspaceRestoreAllowed(frame.payload.workspace_restore);
1062
1071
  const requestedCopyRecord = requestedCopy && typeof requestedCopy === "object"
1063
1072
  ? requestedCopy
1064
1073
  : undefined;
@@ -1079,11 +1088,7 @@ export class AgentServiceSandboxClient {
1079
1088
  typeof copyId !== "string" ||
1080
1089
  copyId.length < 1 ||
1081
1090
  typeof requestedCopySource !== "string" ||
1082
- requestedCopySource.length < 1))
1083
- || (requestedRestore !== null && requestedRestore !== undefined &&
1084
- (typeof requestedRestore !== "object" || Array.isArray(requestedRestore)))
1085
- || (requestedCopy !== null && requestedCopy !== undefined &&
1086
- requestedRestore !== null && requestedRestore !== undefined)) {
1091
+ requestedCopySource.length < 1))) {
1087
1092
  throw new Error("invalid session.open payload");
1088
1093
  }
1089
1094
  if (session.courseRunId !== null && session.courseRunId !== courseRunId) {
@@ -1097,165 +1102,6 @@ export class AgentServiceSandboxClient {
1097
1102
  const isNewSession = session.courseRunId === null;
1098
1103
  let markerPath = null;
1099
1104
  try {
1100
- if (requestedRestore && typeof requestedRestore === "object") {
1101
- const raw = requestedRestore;
1102
- const descriptorRaw = raw.descriptor;
1103
- const controlFence = raw.control_fence;
1104
- let restoreControlToken = raw.control_token;
1105
- if (!controlFence || typeof controlFence !== "object" || Array.isArray(controlFence)) {
1106
- throw new Error("invalid workspace restore control fence");
1107
- }
1108
- if (typeof restoreControlToken !== "string" || restoreControlToken.length < 32) {
1109
- throw new Error("invalid workspace restore control token");
1110
- }
1111
- if (!descriptorRaw || !Array.isArray(raw.entries)) {
1112
- throw new Error("invalid workspace restore payload");
1113
- }
1114
- const descriptor = {
1115
- sandboxId: this.options.sandboxId,
1116
- sandboxGeneration: this.sandboxGeneration,
1117
- runtimeSessionId: sessionId,
1118
- continuityState: descriptorRaw.continuity_state,
1119
- continuityErrorCode: descriptorRaw.continuity_error_code,
1120
- snapshotId: descriptorRaw.snapshot_id,
1121
- revision: Number(descriptorRaw.revision),
1122
- contentSha256: descriptorRaw.content_sha256,
1123
- expectedFileCount: Number(descriptorRaw.expected_file_count),
1124
- expectedEntryCount: Number(descriptorRaw.expected_entry_count),
1125
- expectedTotalBytes: Number(descriptorRaw.expected_total_bytes),
1126
- };
1127
- const workspaceDirectory = runtimeSessionWorkspaceDir(sessionId, this.sandboxGeneration);
1128
- const requiredFreeBytes = Math.max(WORKSPACE_SNAPSHOT_POLICY_V1.requiredRestoreFreeBytes, descriptor.expectedTotalBytes + WORKSPACE_SNAPSHOT_POLICY_V1.requiredRestoreFreeBytes);
1129
- // Fail before issuing or consuming any object grant. The download staging is a
1130
- // sibling on this same bounded backing filesystem and is renamed, not copied,
1131
- // into the atomic restore staging tree.
1132
- assertWorkspaceRestoreCapacity({
1133
- workspaceDirectory,
1134
- descriptor,
1135
- requiredFreeBytes,
1136
- entryCopies: 2,
1137
- });
1138
- const entries = [];
1139
- const predownloadRoot = path.join(path.dirname(workspaceDirectory), `.botlearn-predownload-${sessionId}-${randomUUID()}`);
1140
- mkdirSync(predownloadRoot, { recursive: true, mode: 0o700 });
1141
- try {
1142
- let nextCursor = null;
1143
- do {
1144
- if (nextCursor !== null && nextCursor.length > 2048) {
1145
- throw new Error("invalid workspace restore cursor");
1146
- }
1147
- const pageSignal = AbortSignal.timeout(60_000);
1148
- const response = await fetch(`${agentServiceHttpBase(this.options.wsUrl)}/runtime-sessions/${sessionId}/restore-grants`, {
1149
- method: "POST",
1150
- headers: {
1151
- Authorization: `Bearer ${restoreControlToken}`,
1152
- "Content-Type": "application/json",
1153
- },
1154
- body: JSON.stringify({
1155
- policy_id: WORKSPACE_SNAPSHOT_POLICY_V1.policyId,
1156
- fence: controlFence,
1157
- cursor: nextCursor,
1158
- // Fetch one canonical entry at a time so a grant is consumed as soon as
1159
- // it is signed, including deployments using the minimum 60-second TTL.
1160
- max_entries: 1,
1161
- }),
1162
- signal: pageSignal,
1163
- });
1164
- if (!response.ok) {
1165
- let code = "workspace_restore_unavailable";
1166
- let retryable = response.status >= 500 || response.status === 408 ||
1167
- response.status === 429;
1168
- try {
1169
- const rejected = await response.json();
1170
- const detail = rejected.detail;
1171
- if (detail && typeof detail === "object" && !Array.isArray(detail)) {
1172
- const stableCode = detail.code;
1173
- if (typeof stableCode === "string" && stableCode.length <= 120) {
1174
- code = stableCode;
1175
- }
1176
- if (typeof detail.retryable === "boolean") {
1177
- retryable = detail.retryable;
1178
- }
1179
- }
1180
- }
1181
- catch {
1182
- // Content-free fallback retains the stable local failure code.
1183
- }
1184
- throw new WorkspaceRestoreError(code, retryable);
1185
- }
1186
- const page = await response.json();
1187
- if (!Array.isArray(page.entries))
1188
- throw new Error("invalid workspace restore page");
1189
- if (typeof page.control_token !== "string" || page.control_token.length < 32) {
1190
- throw new Error("invalid refreshed workspace restore control token");
1191
- }
1192
- restoreControlToken = page.control_token;
1193
- const pageFiles = [];
1194
- for (const value of page.entries) {
1195
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1196
- throw new Error("invalid workspace restore entry");
1197
- }
1198
- const item = value;
1199
- if (item.entry_type === "directory") {
1200
- entries.push({ type: "directory", path: String(item.path), mode: "0700" });
1201
- continue;
1202
- }
1203
- if (item.entry_type !== "file" || !item.grant || typeof item.grant !== "object") {
1204
- throw new Error("invalid workspace restore file grant");
1205
- }
1206
- const entry = {
1207
- type: "file",
1208
- path: String(item.path),
1209
- mode: item.mode,
1210
- sizeBytes: Number(item.size_bytes),
1211
- sha256: String(item.sha256),
1212
- };
1213
- entries.push(entry);
1214
- pageFiles.push({ entry, grant: item.grant });
1215
- }
1216
- // Validate all paths and duplicates before using any page path locally.
1217
- validateWorkspaceEntrySet({ schemaVersion: "agent-workspace-entry-set/1", entries }, WORKSPACE_SNAPSHOT_POLICY_V1.limits);
1218
- for (const { entry, grant } of pageFiles) {
1219
- if (typeof grant.url !== "string")
1220
- throw new Error("workspace restore grant missing");
1221
- const destination = path.join(predownloadRoot, ...entry.path.split("/"));
1222
- mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
1223
- const part = `${destination}.part`;
1224
- const downloadSignal = AbortSignal.timeout(60_000);
1225
- const download = await fetch(grant.url, {
1226
- method: "GET",
1227
- headers: grant.headers,
1228
- signal: downloadSignal,
1229
- });
1230
- if (!download.ok)
1231
- throw new Error(`workspace_restore_download_${download.status}`);
1232
- if (!download.body)
1233
- throw new Error("workspace_restore_download_body_missing");
1234
- await pipeline(Readable.fromWeb(download.body), createWriteStream(part, { mode: 0o600 }), { signal: downloadSignal });
1235
- renameSync(part, destination);
1236
- }
1237
- const rawNext = page.next_cursor;
1238
- if (rawNext !== null && rawNext !== undefined && typeof rawNext !== "string") {
1239
- throw new Error("invalid workspace restore cursor");
1240
- }
1241
- nextCursor = rawNext ?? null;
1242
- } while (nextCursor !== null);
1243
- await restoreWorkspaceSnapshot({
1244
- workspaceDirectory,
1245
- controlDirectory: path.join(ensureDaemonHome(), "agent-service-sandboxes", this.options.sandboxId, "workspace-control"),
1246
- descriptor,
1247
- entrySet: { schemaVersion: "agent-workspace-entry-set/1", entries },
1248
- limits: WORKSPACE_SNAPSHOT_POLICY_V1.limits,
1249
- requiredFreeBytes,
1250
- downloadFile: async (entry, partPath) => {
1251
- renameSync(path.join(predownloadRoot, ...entry.path.split("/")), partPath);
1252
- },
1253
- });
1254
- }
1255
- finally {
1256
- rmSync(predownloadRoot, { recursive: true, force: true });
1257
- }
1258
- }
1259
1105
  if (typeof requestedCopySource === "string" && typeof copyId === "string") {
1260
1106
  if (session.workspaceCopyReceipt !== null) {
1261
1107
  const receipt = session.workspaceCopyReceipt;
@@ -1299,14 +1145,6 @@ export class AgentServiceSandboxClient {
1299
1145
  });
1300
1146
  return;
1301
1147
  }
1302
- if (requestedRestore) {
1303
- const restoreError = error instanceof WorkspaceRestoreError ? error : null;
1304
- await this.sendSessionFrame("session.open_failed", sessionId, {
1305
- error_code: restoreError?.code ?? "workspace_restore_unavailable",
1306
- retryable: restoreError?.retryable ?? true,
1307
- });
1308
- return;
1309
- }
1310
1148
  throw error;
1311
1149
  }
1312
1150
  session.courseRunId = courseRunId;
@@ -1322,41 +1160,14 @@ export class AgentServiceSandboxClient {
1322
1160
  if (markerPath !== null) {
1323
1161
  await finalizeRuntimeSessionWorkspaceCopy(markerPath);
1324
1162
  }
1325
- try {
1326
- if (!nasWorkspaceEnabled()) {
1327
- await applyRuntimeWorkspaceQuota(sessionId, this.sandboxGeneration);
1328
- }
1329
- }
1330
- catch (error) {
1331
- if (requestedRestore) {
1332
- await this.sendSessionFrame("session.open_failed", sessionId, {
1333
- error_code: "workspace_restore_unavailable",
1334
- });
1335
- return;
1336
- }
1337
- if (requestedCopy) {
1338
- await this.sendSessionFrame("session.open_failed", sessionId, {
1339
- error_code: "workspace_migration_io_failed",
1340
- });
1341
- return;
1342
- }
1343
- throw error;
1344
- }
1345
1163
  this.state.sessions[sessionId] = session;
1346
1164
  this.persist();
1347
- const materialization = readWorkspaceMaterializationMarker(this.options.sandboxId, sessionId);
1165
+ // NAS 是唯一的 workspace 权威来源,所以 session.opened 不携带 snapshot 物化标记:
1166
+ // 挂载证明只说明当前 generation 已挂上 NAS,不要求运行目录等于某个 committed revision。
1348
1167
  await this.sendSessionFrame("session.opened", sessionId, {
1349
1168
  workspace_ref: session.workspaceRef,
1350
1169
  native_session_id: session.nativeSessionId,
1351
- ...(materialization
1352
- ? {
1353
- workspace_marker_version: 1,
1354
- workspace_continuity_state: materialization.workspaceContinuityState,
1355
- workspace_snapshot_id: materialization.snapshotId,
1356
- workspace_revision: materialization.revision,
1357
- workspace_content_sha256: materialization.contentSha256,
1358
- }
1359
- : {}),
1170
+ workspace_authority: "nas",
1360
1171
  ...(session.workspaceCopyReceipt !== null
1361
1172
  ? { workspace_copy_receipt: session.workspaceCopyReceipt }
1362
1173
  : {}),
@@ -1420,17 +1231,11 @@ export class AgentServiceSandboxClient {
1420
1231
  freezeProof,
1421
1232
  });
1422
1233
  checkpointCommitted = true;
1423
- const marker = readWorkspaceMaterializationMarker(this.options.sandboxId, sessionId);
1424
- if (!marker)
1425
- throw new Error("workspace checkpoint marker missing");
1234
+ // committed revision 只是派生投影,不改变运行目录,所以这里不上报物化标记。
1426
1235
  await this.sendSessionFrame("session.opened", sessionId, {
1427
1236
  workspace_ref: session.workspaceRef,
1428
1237
  native_session_id: session.nativeSessionId,
1429
- workspace_marker_version: 1,
1430
- workspace_continuity_state: marker.workspaceContinuityState,
1431
- workspace_snapshot_id: marker.snapshotId,
1432
- workspace_revision: marker.revision,
1433
- workspace_content_sha256: marker.contentSha256,
1238
+ workspace_authority: "nas",
1434
1239
  });
1435
1240
  await this.sendCommandAck(frame, "ok");
1436
1241
  if (!lifecycleCheckpoint)
package/dist/index.d.ts CHANGED
@@ -12,11 +12,9 @@ export * from "./run-dispatcher.js";
12
12
  export * from "./run-queue.js";
13
13
  export * from "./workspace.js";
14
14
  export * from "./workspace-entry-set.js";
15
- export * from "./workspace-materialization.js";
16
15
  export * from "./workspace-snapshot-staging.js";
17
16
  export * from "./workspace-snapshot-policy.js";
18
17
  export * from "./workspace-snapshot-control.js";
19
- export * from "./workspace-restore.js";
20
18
  export * from "./transcript.js";
21
19
  export * from "./file-candidates.js";
22
20
  export * from "./doctor.js";
package/dist/index.js CHANGED
@@ -12,11 +12,9 @@ export * from "./run-dispatcher.js";
12
12
  export * from "./run-queue.js";
13
13
  export * from "./workspace.js";
14
14
  export * from "./workspace-entry-set.js";
15
- export * from "./workspace-materialization.js";
16
15
  export * from "./workspace-snapshot-staging.js";
17
16
  export * from "./workspace-snapshot-policy.js";
18
17
  export * from "./workspace-snapshot-control.js";
19
- export * from "./workspace-restore.js";
20
18
  export * from "./transcript.js";
21
19
  export * from "./file-candidates.js";
22
20
  export * from "./doctor.js";
package/dist/redaction.js CHANGED
@@ -25,6 +25,7 @@ const QUOTED_JSON_SECRET_PATTERN = new RegExp(`(["'])(${SECRET_KEY_NAME_SOURCE})
25
25
  const QUOTED_ARGV_SECRET_PATTERN = new RegExp(`(["'])(${SECRET_FLAG_SOURCE})\\1(\\s*,\\s*)(["'])([^"'\\\\]*(?:\\\\.[^"'\\\\]*)*)\\4`, "gi");
26
26
  // blic_ 是课程 install code 前缀,与 runtime token 前缀一并脱敏。
27
27
  const SECRET_PREFIX_PATTERN = /\b(blic_|drt_|dit_|gho_|ghp_|sk-)[A-Za-z0-9_-]+/g;
28
+ const SIGNED_OBJECT_URL_PATTERN = /https?:\/\/[^\s"'<>]*[?&](?:X-Amz-Algorithm|X-Amz-Credential|X-Amz-Signature|X-Amz-Security-Token)=[^\s"'<>]*/gi;
28
29
  const SECRET_VALUE_PATTERNS = [
29
30
  [/\b(Bearer\s+)[^\s"']+/gi, `$1${REDACTED}`],
30
31
  [new RegExp(`(^|[\\s])(${SECRET_FLAG_SOURCE})=([^\\s"']+)`, "gi"), `$1$2=${REDACTED}`],
@@ -83,6 +84,7 @@ export function assertNoInjectedCredentials(data, additionalSecrets = []) {
83
84
  }
84
85
  export function redactSecretString(text, additionalSecrets = []) {
85
86
  return redactInjectedCredentials(text, additionalSecrets)
87
+ .replace(SIGNED_OBJECT_URL_PATTERN, REDACTED)
86
88
  .replace(/\b(Bearer\s+)[^\s"']+/gi, `$1${REDACTED}`)
87
89
  .replace(new RegExp(`(^|[\\s])(${SECRET_FLAG_SOURCE})=([^\\s"']+)`, "gi"), `$1$2=${REDACTED}`)
88
90
  .replace(new RegExp(`(^|[\\s])(${SECRET_FLAG_SOURCE})(\\s+)([^\\s"']+)`, "gi"), `$1$2$3${REDACTED}`)
@@ -16,7 +16,7 @@ export declare function runtimeChildIdentity(env?: NodeJS.ProcessEnv): {
16
16
  /**
17
17
  * Build the fixed control-to-runtime privilege boundary used by managed sandboxes.
18
18
  *
19
- * E2B runs the daemon as botlearn-control and grants it one fixed sudoers command.
19
+ * The legacy image boundary may grant botlearn-control one fixed sudoers command.
20
20
  * Platforms that inherit no_new_privs cannot use sudo, so their root supervisor daemon
21
21
  * invokes the same immutable launcher directly; the launcher clears supplementary groups
22
22
  * and performs the one-way uid/gid drop. BYOA retains direct-spawn behavior.
@@ -96,7 +96,7 @@ export function runtimeChildIdentity(env = process.env) {
96
96
  /**
97
97
  * Build the fixed control-to-runtime privilege boundary used by managed sandboxes.
98
98
  *
99
- * E2B runs the daemon as botlearn-control and grants it one fixed sudoers command.
99
+ * The legacy image boundary may grant botlearn-control one fixed sudoers command.
100
100
  * Platforms that inherit no_new_privs cannot use sudo, so their root supervisor daemon
101
101
  * invokes the same immutable launcher directly; the launcher clears supplementary groups
102
102
  * and performs the one-way uid/gid drop. BYOA retains direct-spawn behavior.
@@ -1000,6 +1000,17 @@ function normalizeDeepseekEvent(eventName, payload, seq) {
1000
1000
  status: deepseekToolFailed(payload) ? "error" : "completed",
1001
1001
  };
1002
1002
  }
1003
+ if (eventName === "item.delta" && isDeepseekReasoningEvent(eventName, payload)) {
1004
+ const text = extractDeepseekDelta(payload);
1005
+ return text
1006
+ ? {
1007
+ raw: { event: eventName, payload },
1008
+ kind: "thinking",
1009
+ seq,
1010
+ text,
1011
+ }
1012
+ : null;
1013
+ }
1003
1014
  if (eventName === "item.delta" && isAgentMessageDelta(payload)) {
1004
1015
  return {
1005
1016
  raw: { event: eventName, payload },
@@ -19,7 +19,7 @@ const MANAGED_PATH = [
19
19
  "/opt/node/22.23.1/bin",
20
20
  "/usr/bin",
21
21
  "/bin",
22
- // E2B materializes this directory as runtime-writable. Keep it last and never
22
+ // The managed image exposes this directory as runtime-writable. Keep it last and never
23
23
  // use it for a managed control-plane executable.
24
24
  "/usr/local/bin",
25
25
  ].join(":");
@@ -12,6 +12,7 @@ const CALL_FIELDS = new Set([
12
12
  "arguments",
13
13
  "args",
14
14
  "input",
15
+ "detail",
15
16
  "command",
16
17
  "cmd",
17
18
  "cwd",
@@ -27,6 +28,7 @@ const CALL_FIELDS = new Set([
27
28
  const RESULT_FIELDS = new Set([
28
29
  "result",
29
30
  "output",
31
+ "detail",
30
32
  "stdout",
31
33
  "stderr",
32
34
  "exit_code",
@@ -12,7 +12,6 @@ export interface WorkspaceCapacityProbes {
12
12
  backingFileBytes(imagePath: string): bigint;
13
13
  }
14
14
  export declare function parseWorkspaceMountInfo(text: string): MountInfoEntry[];
15
- export declare function nasWorkspaceEnabled(env?: NodeJS.ProcessEnv): boolean;
16
15
  export declare function workspaceFilesystemFlushCommand(env?: NodeJS.ProcessEnv): [string, string[]] | null;
17
16
  export declare function flushWorkspaceFilesystem(env?: NodeJS.ProcessEnv): Promise<void>;
18
17
  export declare function isWorkspaceQuotaExceededError(error: unknown): boolean;
@@ -1,7 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { readFileSync, statfsSync, statSync } from "node:fs";
3
3
  import { promisify } from "node:util";
4
- import { assertRuntimeWorkspaceQuota } from "./workspace-quota.js";
5
4
  export const WORKSPACE_IMAGE_FORMAT = "botlearn-workspace-image/1";
6
5
  export const WORKSPACE_IMAGE_LOGICAL_BYTES = 3 * 1024 * 1024 * 1024;
7
6
  const RUNTIME_LAUNCHER = "/opt/botlearn/bin/botlearn-runtime-launcher";
@@ -35,12 +34,7 @@ export function parseWorkspaceMountInfo(text) {
35
34
  };
36
35
  });
37
36
  }
38
- export function nasWorkspaceEnabled(env = process.env) {
39
- return env.BOTLEARN_WORKSPACE_IMAGE_FORMAT !== undefined;
40
- }
41
37
  export function workspaceFilesystemFlushCommand(env = process.env) {
42
- if (!nasWorkspaceEnabled(env))
43
- return null;
44
38
  if (env.BOTLEARN_RUNTIME_LAUNCH_MODE === "direct-uid") {
45
39
  return [RUNTIME_LAUNCHER, ["--workspace-filesystem-flush"]];
46
40
  }
@@ -82,10 +76,6 @@ export function isWorkspaceQuotaExceededError(error) {
82
76
  || (candidate.cause !== undefined && isWorkspaceQuotaExceededError(candidate.cause));
83
77
  }
84
78
  export function assertWorkspaceFilesystem(workspaceRoot, env = process.env, mountInfo, capacityProbes) {
85
- if (!nasWorkspaceEnabled(env)) {
86
- assertRuntimeWorkspaceQuota(workspaceRoot);
87
- return;
88
- }
89
79
  if (env.BOTLEARN_WORKSPACE_IMAGE_FORMAT !== WORKSPACE_IMAGE_FORMAT
90
80
  || env.BOTLEARN_WORKSPACE_LOGICAL_BYTES !== String(WORKSPACE_IMAGE_LOGICAL_BYTES)
91
81
  || !/^[0-9a-f]{64}$/u.test(env.BOTLEARN_WORKSPACE_SCOPE_DIGEST ?? "")) {
@@ -1,7 +1,6 @@
1
1
  import { createReadStream, rmSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { ensureDaemonHome } from "./auth-store.js";
4
- import { writeWorkspaceMaterializationMarker } from "./workspace-materialization.js";
5
4
  import { WORKSPACE_SNAPSHOT_POLICY_V1 } from "./workspace-snapshot-policy.js";
6
5
  import { stageWorkspaceSnapshot } from "./workspace-snapshot-staging.js";
7
6
  export class WorkspaceSnapshotControlError extends Error {
@@ -95,19 +94,6 @@ export async function checkpointWorkspace(options) {
95
94
  entry_set: entrySet,
96
95
  }, signal);
97
96
  if (proposal.unchanged) {
98
- if (proposal.revision === 0 && proposal.snapshot_id === null) {
99
- writeWorkspaceMaterializationMarker({
100
- schemaVersion: "agent-workspace-materialization/1",
101
- sandboxId: scope.sandboxId,
102
- sandboxGeneration: scope.sandboxGeneration,
103
- runtimeSessionId: scope.runtimeSessionId,
104
- workspaceContinuityState: "healthy",
105
- snapshotId: null,
106
- revision: 0,
107
- contentSha256: null,
108
- writtenAt: new Date().toISOString(),
109
- });
110
- }
111
97
  return {
112
98
  revision: proposal.revision,
113
99
  snapshotId: proposal.snapshot_id,
@@ -146,17 +132,6 @@ export async function checkpointWorkspace(options) {
146
132
  committed.content_sha256 === null) {
147
133
  throw new WorkspaceSnapshotControlError(committed.error_code || "workspace_snapshot_verification_failed", false);
148
134
  }
149
- writeWorkspaceMaterializationMarker({
150
- schemaVersion: "agent-workspace-materialization/1",
151
- sandboxId: scope.sandboxId,
152
- sandboxGeneration: scope.sandboxGeneration,
153
- runtimeSessionId: scope.runtimeSessionId,
154
- workspaceContinuityState: "healthy",
155
- snapshotId: committed.snapshot_id,
156
- revision: committed.revision,
157
- contentSha256: committed.content_sha256,
158
- writtenAt: new Date().toISOString(),
159
- });
160
135
  return {
161
136
  revision: committed.revision,
162
137
  snapshotId: committed.snapshot_id,
@@ -20,5 +20,5 @@ export declare const WORKSPACE_SNAPSHOT_POLICY_V1: {
20
20
  readonly checkpointReservationBytes: number;
21
21
  readonly restorePageEntries: 25;
22
22
  };
23
- /** Wire representation validated exactly by Agent Service during the v0.3 handshake. */
23
+ /** Wire representation validated exactly by Agent Service during the handshake. */
24
24
  export declare function workspaceSnapshotPolicyCapability(): Record<string, unknown>;
@@ -1,4 +1,6 @@
1
- import { RUNTIME_WORKSPACE_INODE_QUOTA, RUNTIME_WORKSPACE_QUOTA_BYTES, } from "./workspace-quota.js";
1
+ /** runtime 目录配额;与 Course Service WorkspaceSnapshotPolicyV1 逐字段一致。 */
2
+ const RUNTIME_WORKSPACE_QUOTA_BYTES = 512 * 1024 * 1024;
3
+ const RUNTIME_WORKSPACE_INODE_QUOTA = 2_000;
2
4
  /**
3
5
  * Protocol limits frozen with Course Service's WorkspaceSnapshotPolicyV1.
4
6
  * Deployment may enable the capability, but must never override these values.
@@ -21,7 +23,7 @@ export const WORKSPACE_SNAPSHOT_POLICY_V1 = {
21
23
  checkpointReservationBytes: 512 * 1024 * 1024,
22
24
  restorePageEntries: 25,
23
25
  };
24
- /** Wire representation validated exactly by Agent Service during the v0.3 handshake. */
26
+ /** Wire representation validated exactly by Agent Service during the handshake. */
25
27
  export function workspaceSnapshotPolicyCapability() {
26
28
  return {
27
29
  policy_id: WORKSPACE_SNAPSHOT_POLICY_V1.policyId,
@@ -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;