@botlearn-course/daemon 0.0.20-beta.14 → 0.0.20-beta.16
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/agent-service-sandbox.d.ts +9 -4
- package/dist/agent-service-sandbox.js +81 -327
- package/dist/agent-service-ws-protocol.d.ts +1 -1
- package/dist/agent-service-ws-protocol.js +2 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/dist/run-dispatcher.js +4 -3
- package/dist/runtimes/deepseek-tui-base-prompt.d.ts +4 -0
- package/dist/runtimes/deepseek-tui-base-prompt.js +86 -0
- package/dist/runtimes/deepseek-tui.js +8 -1
- package/dist/runtimes/engine.d.ts +2 -0
- package/dist/runtimes/engine.js +1 -0
- package/dist/workspace-filesystem.d.ts +0 -1
- package/dist/workspace-filesystem.js +0 -10
- package/dist/workspace-snapshot-control.js +0 -25
- package/dist/workspace-snapshot-policy.d.ts +1 -1
- package/dist/workspace-snapshot-policy.js +4 -2
- package/dist/workspace.d.ts +10 -66
- package/dist/workspace.js +44 -594
- package/package.json +1 -1
- package/dist/workspace-materialization.d.ts +0 -16
- package/dist/workspace-materialization.js +0 -136
- package/dist/workspace-quota.d.ts +0 -4
- package/dist/workspace-quota.js +0 -42
- package/dist/workspace-restore.d.ts +0 -42
- package/dist/workspace-restore.js +0 -347
|
@@ -3,8 +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(
|
|
7
|
-
|
|
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;
|
|
8
13
|
export interface AgentServiceSandboxOptions {
|
|
9
14
|
wsUrl: string;
|
|
10
15
|
sandboxId: string;
|
|
@@ -99,8 +104,8 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
99
104
|
private handleTurnStart;
|
|
100
105
|
private handleTurnCancel;
|
|
101
106
|
/**
|
|
102
|
-
* 幂等关闭一个 runtime session:终止其 runtime
|
|
103
|
-
*
|
|
107
|
+
* 幂等关闭一个 runtime session:终止其 runtime 子进程并清空 session-local state。
|
|
108
|
+
* Durable Workspace 由独立身份拥有,关闭执行上下文不会删除学员文件。
|
|
104
109
|
*/
|
|
105
110
|
private closeSession;
|
|
106
111
|
private reconcileInterruptedCommand;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
@@ -11,17 +8,14 @@ import { activationRuntimeEnv, runtimeChildEnv } from "./runtime-env.js";
|
|
|
11
8
|
import { redactSecretString } from "./redaction.js";
|
|
12
9
|
import { parseRuntimeSkillProviderGrantSet, prepareRuntimeSkillProvider, RuntimeSkillProviderError, } from "./runtime-skills.js";
|
|
13
10
|
import { RunDispatcher, } from "./run-dispatcher.js";
|
|
14
|
-
import {
|
|
11
|
+
import { ensureRuntimeSessionDirectories, ensureRuntimeSessionWorkspace, exposeRuntimeSessionWorkspace, removeRuntimeSessionWorkspace, revokeRuntimeSessionWorkspace, runtimeSessionWorkspaceDir, } from "./workspace.js";
|
|
15
12
|
import { readWorkspaceFile, WorkspaceFileReadError } from "./workspace-file-read.js";
|
|
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 {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
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,15 +35,21 @@ 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;
|
|
44
|
-
export function workspaceDurabilityCapabilities(
|
|
38
|
+
export function workspaceDurabilityCapabilities() {
|
|
45
39
|
return [
|
|
46
40
|
"workspace_writer_freeze_v1",
|
|
47
41
|
"workspace_snapshot_v2",
|
|
48
|
-
|
|
42
|
+
"workspace_nas_v1",
|
|
43
|
+
"workspace_identity_v1",
|
|
49
44
|
];
|
|
50
45
|
}
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
/**
|
|
47
|
+
* NAS 是运行时 workspace 的唯一持久事实来源,committed snapshot 只供前端查看。
|
|
48
|
+
* 任何 restore payload 都意味着对端仍在下发已删除的 S3 回写指令,必须 fail closed,
|
|
49
|
+
* 否则可能用落后的派生快照覆盖 NAS 中较新的内容。
|
|
50
|
+
*/
|
|
51
|
+
export function assertWorkspaceRestoreAllowed(requestedRestore) {
|
|
52
|
+
if (requestedRestore !== null && requestedRestore !== undefined) {
|
|
53
53
|
throw new Error("workspace_restore_forbidden_for_nas");
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -243,8 +243,8 @@ function sandboxStatePath(sandboxId) {
|
|
|
243
243
|
function emptySessionState() {
|
|
244
244
|
return {
|
|
245
245
|
courseRunId: null,
|
|
246
|
+
workspaceId: null,
|
|
246
247
|
workspaceRef: null,
|
|
247
|
-
workspaceCopyReceipt: null,
|
|
248
248
|
runtimeId: null,
|
|
249
249
|
nativeSessionId: null,
|
|
250
250
|
contextRevision: 0,
|
|
@@ -262,8 +262,8 @@ function normalizeSessionState(value) {
|
|
|
262
262
|
return base;
|
|
263
263
|
return {
|
|
264
264
|
courseRunId: typeof value.courseRunId === "string" ? value.courseRunId : null,
|
|
265
|
+
workspaceId: typeof value.workspaceId === "string" ? value.workspaceId : null,
|
|
265
266
|
workspaceRef: typeof value.workspaceRef === "string" ? value.workspaceRef : null,
|
|
266
|
-
workspaceCopyReceipt: (value.workspaceCopyReceipt && typeof value.workspaceCopyReceipt === "object") ? value.workspaceCopyReceipt : null,
|
|
267
267
|
runtimeId: typeof value.runtimeId === "string" ? value.runtimeId : null,
|
|
268
268
|
nativeSessionId: typeof value.nativeSessionId === "string" ? value.nativeSessionId : null,
|
|
269
269
|
contextRevision: Number(value.contextRevision ?? 0),
|
|
@@ -434,10 +434,10 @@ export class AgentServiceSandboxClient {
|
|
|
434
434
|
if (!activation || activation.activationId !== scope.activationId) {
|
|
435
435
|
throw new Error("managed turn activation context is unavailable");
|
|
436
436
|
}
|
|
437
|
-
const prepared = ensureRuntimeSessionWorkspace(scope.sessionId, this.sandboxGeneration, payload.agent_run_id);
|
|
437
|
+
const prepared = ensureRuntimeSessionWorkspace(scope.sessionId, this.sandboxGeneration, payload.agent_run_id, session.workspaceId ?? scope.sessionId);
|
|
438
438
|
// ensureRuntimeSessionWorkspace creates missing paths with the closed-by-default
|
|
439
439
|
// mode; re-expose only after the activation fence above has been checked.
|
|
440
|
-
exposeRuntimeSessionWorkspace(scope.sessionId, this.sandboxGeneration);
|
|
440
|
+
exposeRuntimeSessionWorkspace(scope.sessionId, this.sandboxGeneration, session.workspaceId ?? scope.sessionId);
|
|
441
441
|
this.currentTurnSessionId = scope.sessionId;
|
|
442
442
|
const runtimeEnv = runtimeChildEnv(process.env);
|
|
443
443
|
delete runtimeEnv.DEEPSEEK_API_KEY;
|
|
@@ -536,6 +536,26 @@ export class AgentServiceSandboxClient {
|
|
|
536
536
|
// there is no safe monotonic ordering from which to infer that an old replay died.
|
|
537
537
|
session.completedActivations.push(pending.activationId);
|
|
538
538
|
}
|
|
539
|
+
const cleanupFrame = createSandboxFrame({
|
|
540
|
+
type: "activation.cleaned",
|
|
541
|
+
sandboxId: this.options.sandboxId,
|
|
542
|
+
sandboxGeneration: this.sandboxGeneration,
|
|
543
|
+
connectionEpoch: this.connectionEpoch,
|
|
544
|
+
seq: this.nextOutboundSeq(),
|
|
545
|
+
runtimeSessionId: sessionId,
|
|
546
|
+
agentRunId: pending.agentRunId,
|
|
547
|
+
workerAttempt: pending.workerAttempt,
|
|
548
|
+
activationId: pending.activationId,
|
|
549
|
+
});
|
|
550
|
+
session.spool.push(cleanupFrame);
|
|
551
|
+
try {
|
|
552
|
+
this.enforceSpoolLimit();
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
session.spool.pop();
|
|
556
|
+
this.persist();
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
539
559
|
const commandId = `run:${pending.agentRunId}:${pending.workerAttempt}`;
|
|
540
560
|
if (!session.completedCommands.includes(commandId)) {
|
|
541
561
|
session.completedCommands.push(commandId);
|
|
@@ -547,6 +567,8 @@ export class AgentServiceSandboxClient {
|
|
|
547
567
|
if (this.currentTurnSessionId === sessionId)
|
|
548
568
|
this.currentTurnSessionId = null;
|
|
549
569
|
this.persist();
|
|
570
|
+
if (this.runtimeLogHandshakeReady)
|
|
571
|
+
await this.sendFrame(cleanupFrame);
|
|
550
572
|
}
|
|
551
573
|
async recoverPendingActivationCleanups() {
|
|
552
574
|
for (const sessionId of Object.keys(this.state.sessions)) {
|
|
@@ -600,7 +622,7 @@ export class AgentServiceSandboxClient {
|
|
|
600
622
|
this.dispatcher.cancelAll();
|
|
601
623
|
for (const [sessionId, session] of Object.entries(this.state.sessions)) {
|
|
602
624
|
session.activationId = null;
|
|
603
|
-
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration);
|
|
625
|
+
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration, session.workspaceId ?? sessionId);
|
|
604
626
|
}
|
|
605
627
|
this.activationContexts.clear();
|
|
606
628
|
this.sessionProfiles.clear();
|
|
@@ -831,9 +853,6 @@ export class AgentServiceSandboxClient {
|
|
|
831
853
|
}
|
|
832
854
|
}
|
|
833
855
|
async handleHello(frame) {
|
|
834
|
-
// A reconnect can race a copy that started on the previous socket. Wait for
|
|
835
|
-
// the serialized lifecycle attempt to converge before deleting crash-left
|
|
836
|
-
// staging and advertising capability on the new connection.
|
|
837
856
|
await this.lifecycleChain;
|
|
838
857
|
if (frame.sandbox_id !== this.options.sandboxId) {
|
|
839
858
|
throw new SandboxClosedError(4403, "sandbox_mismatch");
|
|
@@ -846,9 +865,10 @@ export class AgentServiceSandboxClient {
|
|
|
846
865
|
this.dispatcher.cancelAll();
|
|
847
866
|
await quiesceRuntimeWriters(null);
|
|
848
867
|
this.rejectPending(new Error("Agent Service sandbox generation changed"));
|
|
849
|
-
for (const sessionId of Object.
|
|
850
|
-
revokeRuntimeSessionWorkspace(sessionId, this.state.sandboxGeneration);
|
|
851
|
-
|
|
868
|
+
for (const [sessionId, session] of Object.entries(this.state.sessions)) {
|
|
869
|
+
revokeRuntimeSessionWorkspace(sessionId, this.state.sandboxGeneration, session.workspaceId ?? sessionId);
|
|
870
|
+
// NAS image 跨 generation 保留正文,只清掉本地 daemon 侧的 session 状态。
|
|
871
|
+
removeRuntimeSessionWorkspace(sessionId, this.state.sandboxGeneration, true);
|
|
852
872
|
}
|
|
853
873
|
this.state.sessions = {};
|
|
854
874
|
this.state.sandboxGeneration = frame.sandbox_generation;
|
|
@@ -876,7 +896,6 @@ export class AgentServiceSandboxClient {
|
|
|
876
896
|
: LEGACY_EVENT_ACK_WINDOW;
|
|
877
897
|
this.maxUnackedEvents = Math.min(MAX_EVENT_ACK_WINDOW, ackWindow);
|
|
878
898
|
this.runtimeLogPolicy = runtimeLogPolicy(frame.payload.runtime_log_policy);
|
|
879
|
-
await cleanupRuntimeSessionWorkspaceCopyStaging();
|
|
880
899
|
this.persist();
|
|
881
900
|
assertWorkspaceFilesystem(process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT ?? "");
|
|
882
901
|
assertWorkspaceWriterFreezeSupported();
|
|
@@ -885,7 +904,6 @@ export class AgentServiceSandboxClient {
|
|
|
885
904
|
capabilities: [
|
|
886
905
|
WORKSPACE_FILE_READ_BINARY_CAPABILITY,
|
|
887
906
|
...workspaceDurabilityCapabilities(),
|
|
888
|
-
...(workspaceCopyV1Supported() ? ["workspace_copy_v1"] : []),
|
|
889
907
|
],
|
|
890
908
|
workspace_snapshot_policy: workspaceSnapshotPolicyCapability(),
|
|
891
909
|
daemon_version: this.options.daemonVersion,
|
|
@@ -1065,315 +1083,54 @@ export class AgentServiceSandboxClient {
|
|
|
1065
1083
|
const sessionId = frame.runtime_session_id;
|
|
1066
1084
|
const session = this.state.sessions[sessionId] ?? emptySessionState();
|
|
1067
1085
|
const courseRunId = frame.payload.course_run_id;
|
|
1086
|
+
// Mixed-version reconciliation may replay a pre-Workspace-identity session.open.
|
|
1087
|
+
// Its physical directory was keyed by RuntimeSession id, which is the exact backfill
|
|
1088
|
+
// identity used by the database migration.
|
|
1089
|
+
const requestedWorkspaceId = typeof frame.payload.workspace_id === "string"
|
|
1090
|
+
? frame.payload.workspace_id
|
|
1091
|
+
: session.workspaceId ?? sessionId;
|
|
1068
1092
|
const requestedWorkspaceRef = frame.payload.workspace_ref;
|
|
1069
|
-
|
|
1070
|
-
const requestedRestore = frame.payload.workspace_restore;
|
|
1071
|
-
assertWorkspaceRestoreAllowed(requestedRestore);
|
|
1072
|
-
const requestedCopyRecord = requestedCopy && typeof requestedCopy === "object"
|
|
1073
|
-
? requestedCopy
|
|
1074
|
-
: undefined;
|
|
1075
|
-
const copyId = requestedCopyRecord
|
|
1076
|
-
? requestedCopyRecord.copy_id
|
|
1077
|
-
: undefined;
|
|
1078
|
-
const requestedCopySource = requestedCopyRecord
|
|
1079
|
-
? requestedCopyRecord.source_runtime_session_id
|
|
1080
|
-
: undefined;
|
|
1093
|
+
assertWorkspaceRestoreAllowed(frame.payload.workspace_restore);
|
|
1081
1094
|
if (typeof courseRunId !== "string" ||
|
|
1082
1095
|
courseRunId.length < 1 ||
|
|
1096
|
+
requestedWorkspaceId.length < 1 ||
|
|
1083
1097
|
(requestedWorkspaceRef !== null &&
|
|
1084
1098
|
requestedWorkspaceRef !== undefined &&
|
|
1085
|
-
(typeof requestedWorkspaceRef !== "string" || requestedWorkspaceRef.length < 1))
|
|
1086
|
-
(requestedCopy !== null &&
|
|
1087
|
-
requestedCopy !== undefined &&
|
|
1088
|
-
(typeof requestedCopy !== "object" ||
|
|
1089
|
-
typeof copyId !== "string" ||
|
|
1090
|
-
copyId.length < 1 ||
|
|
1091
|
-
typeof requestedCopySource !== "string" ||
|
|
1092
|
-
requestedCopySource.length < 1))
|
|
1093
|
-
|| (requestedRestore !== null && requestedRestore !== undefined &&
|
|
1094
|
-
(typeof requestedRestore !== "object" || Array.isArray(requestedRestore)))
|
|
1095
|
-
|| (requestedCopy !== null && requestedCopy !== undefined &&
|
|
1096
|
-
requestedRestore !== null && requestedRestore !== undefined)) {
|
|
1099
|
+
(typeof requestedWorkspaceRef !== "string" || requestedWorkspaceRef.length < 1))) {
|
|
1097
1100
|
throw new Error("invalid session.open payload");
|
|
1098
1101
|
}
|
|
1099
1102
|
if (session.courseRunId !== null && session.courseRunId !== courseRunId) {
|
|
1100
1103
|
throw new Error("runtime session course_run_id cannot be rebound");
|
|
1101
1104
|
}
|
|
1105
|
+
if (session.workspaceId !== null && session.workspaceId !== requestedWorkspaceId) {
|
|
1106
|
+
throw new Error("runtime session workspace_id cannot be rebound");
|
|
1107
|
+
}
|
|
1102
1108
|
if (session.workspaceRef !== null &&
|
|
1103
1109
|
typeof requestedWorkspaceRef === "string" &&
|
|
1104
1110
|
session.workspaceRef !== requestedWorkspaceRef) {
|
|
1105
1111
|
throw new Error("runtime session workspace_ref cannot be rebound");
|
|
1106
1112
|
}
|
|
1107
1113
|
const isNewSession = session.courseRunId === null;
|
|
1108
|
-
let markerPath = null;
|
|
1109
|
-
try {
|
|
1110
|
-
if (requestedRestore && typeof requestedRestore === "object") {
|
|
1111
|
-
const raw = requestedRestore;
|
|
1112
|
-
const descriptorRaw = raw.descriptor;
|
|
1113
|
-
const controlFence = raw.control_fence;
|
|
1114
|
-
let restoreControlToken = raw.control_token;
|
|
1115
|
-
if (!controlFence || typeof controlFence !== "object" || Array.isArray(controlFence)) {
|
|
1116
|
-
throw new Error("invalid workspace restore control fence");
|
|
1117
|
-
}
|
|
1118
|
-
if (typeof restoreControlToken !== "string" || restoreControlToken.length < 32) {
|
|
1119
|
-
throw new Error("invalid workspace restore control token");
|
|
1120
|
-
}
|
|
1121
|
-
if (!descriptorRaw || !Array.isArray(raw.entries)) {
|
|
1122
|
-
throw new Error("invalid workspace restore payload");
|
|
1123
|
-
}
|
|
1124
|
-
const descriptor = {
|
|
1125
|
-
sandboxId: this.options.sandboxId,
|
|
1126
|
-
sandboxGeneration: this.sandboxGeneration,
|
|
1127
|
-
runtimeSessionId: sessionId,
|
|
1128
|
-
continuityState: descriptorRaw.continuity_state,
|
|
1129
|
-
continuityErrorCode: descriptorRaw.continuity_error_code,
|
|
1130
|
-
snapshotId: descriptorRaw.snapshot_id,
|
|
1131
|
-
revision: Number(descriptorRaw.revision),
|
|
1132
|
-
contentSha256: descriptorRaw.content_sha256,
|
|
1133
|
-
expectedFileCount: Number(descriptorRaw.expected_file_count),
|
|
1134
|
-
expectedEntryCount: Number(descriptorRaw.expected_entry_count),
|
|
1135
|
-
expectedTotalBytes: Number(descriptorRaw.expected_total_bytes),
|
|
1136
|
-
};
|
|
1137
|
-
const workspaceDirectory = runtimeSessionWorkspaceDir(sessionId, this.sandboxGeneration);
|
|
1138
|
-
const requiredFreeBytes = Math.max(WORKSPACE_SNAPSHOT_POLICY_V1.requiredRestoreFreeBytes, descriptor.expectedTotalBytes + WORKSPACE_SNAPSHOT_POLICY_V1.requiredRestoreFreeBytes);
|
|
1139
|
-
// Fail before issuing or consuming any object grant. The download staging is a
|
|
1140
|
-
// sibling on this same bounded backing filesystem and is renamed, not copied,
|
|
1141
|
-
// into the atomic restore staging tree.
|
|
1142
|
-
assertWorkspaceRestoreCapacity({
|
|
1143
|
-
workspaceDirectory,
|
|
1144
|
-
descriptor,
|
|
1145
|
-
requiredFreeBytes,
|
|
1146
|
-
entryCopies: 2,
|
|
1147
|
-
});
|
|
1148
|
-
const entries = [];
|
|
1149
|
-
const predownloadRoot = path.join(path.dirname(workspaceDirectory), `.botlearn-predownload-${sessionId}-${randomUUID()}`);
|
|
1150
|
-
mkdirSync(predownloadRoot, { recursive: true, mode: 0o700 });
|
|
1151
|
-
try {
|
|
1152
|
-
let nextCursor = null;
|
|
1153
|
-
do {
|
|
1154
|
-
if (nextCursor !== null && nextCursor.length > 2048) {
|
|
1155
|
-
throw new Error("invalid workspace restore cursor");
|
|
1156
|
-
}
|
|
1157
|
-
const pageSignal = AbortSignal.timeout(60_000);
|
|
1158
|
-
const response = await fetch(`${agentServiceHttpBase(this.options.wsUrl)}/runtime-sessions/${sessionId}/restore-grants`, {
|
|
1159
|
-
method: "POST",
|
|
1160
|
-
headers: {
|
|
1161
|
-
Authorization: `Bearer ${restoreControlToken}`,
|
|
1162
|
-
"Content-Type": "application/json",
|
|
1163
|
-
},
|
|
1164
|
-
body: JSON.stringify({
|
|
1165
|
-
policy_id: WORKSPACE_SNAPSHOT_POLICY_V1.policyId,
|
|
1166
|
-
fence: controlFence,
|
|
1167
|
-
cursor: nextCursor,
|
|
1168
|
-
// Fetch one canonical entry at a time so a grant is consumed as soon as
|
|
1169
|
-
// it is signed, including deployments using the minimum 60-second TTL.
|
|
1170
|
-
max_entries: 1,
|
|
1171
|
-
}),
|
|
1172
|
-
signal: pageSignal,
|
|
1173
|
-
});
|
|
1174
|
-
if (!response.ok) {
|
|
1175
|
-
let code = "workspace_restore_unavailable";
|
|
1176
|
-
let retryable = response.status >= 500 || response.status === 408 ||
|
|
1177
|
-
response.status === 429;
|
|
1178
|
-
try {
|
|
1179
|
-
const rejected = await response.json();
|
|
1180
|
-
const detail = rejected.detail;
|
|
1181
|
-
if (detail && typeof detail === "object" && !Array.isArray(detail)) {
|
|
1182
|
-
const stableCode = detail.code;
|
|
1183
|
-
if (typeof stableCode === "string" && stableCode.length <= 120) {
|
|
1184
|
-
code = stableCode;
|
|
1185
|
-
}
|
|
1186
|
-
if (typeof detail.retryable === "boolean") {
|
|
1187
|
-
retryable = detail.retryable;
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
catch {
|
|
1192
|
-
// Content-free fallback retains the stable local failure code.
|
|
1193
|
-
}
|
|
1194
|
-
throw new WorkspaceRestoreError(code, retryable);
|
|
1195
|
-
}
|
|
1196
|
-
const page = await response.json();
|
|
1197
|
-
if (!Array.isArray(page.entries))
|
|
1198
|
-
throw new Error("invalid workspace restore page");
|
|
1199
|
-
if (typeof page.control_token !== "string" || page.control_token.length < 32) {
|
|
1200
|
-
throw new Error("invalid refreshed workspace restore control token");
|
|
1201
|
-
}
|
|
1202
|
-
restoreControlToken = page.control_token;
|
|
1203
|
-
const pageFiles = [];
|
|
1204
|
-
for (const value of page.entries) {
|
|
1205
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1206
|
-
throw new Error("invalid workspace restore entry");
|
|
1207
|
-
}
|
|
1208
|
-
const item = value;
|
|
1209
|
-
if (item.entry_type === "directory") {
|
|
1210
|
-
entries.push({ type: "directory", path: String(item.path), mode: "0700" });
|
|
1211
|
-
continue;
|
|
1212
|
-
}
|
|
1213
|
-
if (item.entry_type !== "file" || !item.grant || typeof item.grant !== "object") {
|
|
1214
|
-
throw new Error("invalid workspace restore file grant");
|
|
1215
|
-
}
|
|
1216
|
-
const entry = {
|
|
1217
|
-
type: "file",
|
|
1218
|
-
path: String(item.path),
|
|
1219
|
-
mode: item.mode,
|
|
1220
|
-
sizeBytes: Number(item.size_bytes),
|
|
1221
|
-
sha256: String(item.sha256),
|
|
1222
|
-
};
|
|
1223
|
-
entries.push(entry);
|
|
1224
|
-
pageFiles.push({ entry, grant: item.grant });
|
|
1225
|
-
}
|
|
1226
|
-
// Validate all paths and duplicates before using any page path locally.
|
|
1227
|
-
validateWorkspaceEntrySet({ schemaVersion: "agent-workspace-entry-set/1", entries }, WORKSPACE_SNAPSHOT_POLICY_V1.limits);
|
|
1228
|
-
for (const { entry, grant } of pageFiles) {
|
|
1229
|
-
if (typeof grant.url !== "string")
|
|
1230
|
-
throw new Error("workspace restore grant missing");
|
|
1231
|
-
const destination = path.join(predownloadRoot, ...entry.path.split("/"));
|
|
1232
|
-
mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
1233
|
-
const part = `${destination}.part`;
|
|
1234
|
-
const downloadSignal = AbortSignal.timeout(60_000);
|
|
1235
|
-
const download = await fetch(grant.url, {
|
|
1236
|
-
method: "GET",
|
|
1237
|
-
headers: grant.headers,
|
|
1238
|
-
signal: downloadSignal,
|
|
1239
|
-
});
|
|
1240
|
-
if (!download.ok)
|
|
1241
|
-
throw new Error(`workspace_restore_download_${download.status}`);
|
|
1242
|
-
if (!download.body)
|
|
1243
|
-
throw new Error("workspace_restore_download_body_missing");
|
|
1244
|
-
await pipeline(Readable.fromWeb(download.body), createWriteStream(part, { mode: 0o600 }), { signal: downloadSignal });
|
|
1245
|
-
renameSync(part, destination);
|
|
1246
|
-
}
|
|
1247
|
-
const rawNext = page.next_cursor;
|
|
1248
|
-
if (rawNext !== null && rawNext !== undefined && typeof rawNext !== "string") {
|
|
1249
|
-
throw new Error("invalid workspace restore cursor");
|
|
1250
|
-
}
|
|
1251
|
-
nextCursor = rawNext ?? null;
|
|
1252
|
-
} while (nextCursor !== null);
|
|
1253
|
-
await restoreWorkspaceSnapshot({
|
|
1254
|
-
workspaceDirectory,
|
|
1255
|
-
controlDirectory: path.join(ensureDaemonHome(), "agent-service-sandboxes", this.options.sandboxId, "workspace-control"),
|
|
1256
|
-
descriptor,
|
|
1257
|
-
entrySet: { schemaVersion: "agent-workspace-entry-set/1", entries },
|
|
1258
|
-
limits: WORKSPACE_SNAPSHOT_POLICY_V1.limits,
|
|
1259
|
-
requiredFreeBytes,
|
|
1260
|
-
downloadFile: async (entry, partPath) => {
|
|
1261
|
-
renameSync(path.join(predownloadRoot, ...entry.path.split("/")), partPath);
|
|
1262
|
-
},
|
|
1263
|
-
});
|
|
1264
|
-
}
|
|
1265
|
-
finally {
|
|
1266
|
-
rmSync(predownloadRoot, { recursive: true, force: true });
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
if (typeof requestedCopySource === "string" && typeof copyId === "string") {
|
|
1270
|
-
if (session.workspaceCopyReceipt !== null) {
|
|
1271
|
-
const receipt = session.workspaceCopyReceipt;
|
|
1272
|
-
if (receipt.copy_id !== copyId ||
|
|
1273
|
-
receipt.source_runtime_session_id !== requestedCopySource ||
|
|
1274
|
-
receipt.target_runtime_session_id !== sessionId ||
|
|
1275
|
-
receipt.source_sandbox_id !== this.options.sandboxId ||
|
|
1276
|
-
receipt.source_generation !== this.sandboxGeneration) {
|
|
1277
|
-
throw new WorkspaceCopyError("workspace_migration_receipt_mismatch");
|
|
1278
|
-
}
|
|
1279
|
-
markerPath = path.join(runtimeSessionWorkspaceDir(sessionId, this.sandboxGeneration), WORKSPACE_COPY_POLICY_V1.markerName);
|
|
1280
|
-
}
|
|
1281
|
-
else if (!isNewSession) {
|
|
1282
|
-
throw new WorkspaceCopyError("workspace_migration_receipt_mismatch");
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
if (isNewSession && typeof requestedCopySource === "string" && typeof copyId === "string") {
|
|
1286
|
-
const sourceSession = this.state.sessions[requestedCopySource];
|
|
1287
|
-
if (!sourceSession ||
|
|
1288
|
-
sourceSession.courseRunId === null ||
|
|
1289
|
-
this.activeSessionId === requestedCopySource ||
|
|
1290
|
-
this.currentTurnSessionId === requestedCopySource) {
|
|
1291
|
-
throw new WorkspaceCopyError("workspace_migration_source_unavailable");
|
|
1292
|
-
}
|
|
1293
|
-
revokeRuntimeSessionWorkspace(requestedCopySource, this.sandboxGeneration);
|
|
1294
|
-
const copied = await copyRuntimeSessionWorkspace({
|
|
1295
|
-
copyId,
|
|
1296
|
-
sourceRuntimeSessionId: requestedCopySource,
|
|
1297
|
-
targetRuntimeSessionId: sessionId,
|
|
1298
|
-
sandboxId: this.options.sandboxId,
|
|
1299
|
-
sandboxGeneration: this.sandboxGeneration,
|
|
1300
|
-
});
|
|
1301
|
-
session.workspaceCopyReceipt = copied.receipt;
|
|
1302
|
-
markerPath = copied.markerPath;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
catch (error) {
|
|
1306
|
-
if (error instanceof WorkspaceCopyError) {
|
|
1307
|
-
await this.sendSessionFrame("session.open_failed", sessionId, {
|
|
1308
|
-
error_code: error.code,
|
|
1309
|
-
});
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
if (requestedRestore) {
|
|
1313
|
-
const restoreError = error instanceof WorkspaceRestoreError ? error : null;
|
|
1314
|
-
await this.sendSessionFrame("session.open_failed", sessionId, {
|
|
1315
|
-
error_code: restoreError?.code ?? "workspace_restore_unavailable",
|
|
1316
|
-
retryable: restoreError?.retryable ?? true,
|
|
1317
|
-
});
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
throw error;
|
|
1321
|
-
}
|
|
1322
1114
|
session.courseRunId = courseRunId;
|
|
1115
|
+
session.workspaceId = requestedWorkspaceId;
|
|
1323
1116
|
session.workspaceRef = session.workspaceRef ?? (typeof requestedWorkspaceRef === "string"
|
|
1324
1117
|
? requestedWorkspaceRef
|
|
1325
1118
|
: `ws_${sessionId.replaceAll("-", "")}_g${this.sandboxGeneration}`);
|
|
1326
1119
|
// Replaying session.open is part of reconnect reconciliation. An already-active
|
|
1327
1120
|
// runtime may still be creating files in this workspace, so never transiently revoke
|
|
1328
1121
|
// its group write/execute access before the following session.activate arrives.
|
|
1329
|
-
if (
|
|
1330
|
-
ensureRuntimeSessionDirectories(sessionId, this.sandboxGeneration);
|
|
1331
|
-
}
|
|
1332
|
-
if (markerPath !== null) {
|
|
1333
|
-
await finalizeRuntimeSessionWorkspaceCopy(markerPath);
|
|
1334
|
-
}
|
|
1335
|
-
try {
|
|
1336
|
-
if (!nasWorkspaceEnabled()) {
|
|
1337
|
-
await applyRuntimeWorkspaceQuota(sessionId, this.sandboxGeneration);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
catch (error) {
|
|
1341
|
-
if (requestedRestore) {
|
|
1342
|
-
await this.sendSessionFrame("session.open_failed", sessionId, {
|
|
1343
|
-
error_code: "workspace_restore_unavailable",
|
|
1344
|
-
});
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
if (requestedCopy) {
|
|
1348
|
-
await this.sendSessionFrame("session.open_failed", sessionId, {
|
|
1349
|
-
error_code: "workspace_migration_io_failed",
|
|
1350
|
-
});
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
throw error;
|
|
1122
|
+
if (isNewSession || session.activationId === null) {
|
|
1123
|
+
ensureRuntimeSessionDirectories(sessionId, this.sandboxGeneration, requestedWorkspaceId);
|
|
1354
1124
|
}
|
|
1355
1125
|
this.state.sessions[sessionId] = session;
|
|
1356
1126
|
this.persist();
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
? null
|
|
1360
|
-
: readWorkspaceMaterializationMarker(this.options.sandboxId, sessionId);
|
|
1127
|
+
// NAS 是唯一的 workspace 权威来源,所以 session.opened 不携带 snapshot 物化标记:
|
|
1128
|
+
// 挂载证明只说明当前 generation 已挂上 NAS,不要求运行目录等于某个 committed revision。
|
|
1361
1129
|
await this.sendSessionFrame("session.opened", sessionId, {
|
|
1130
|
+
workspace_id: session.workspaceId,
|
|
1362
1131
|
workspace_ref: session.workspaceRef,
|
|
1363
1132
|
native_session_id: session.nativeSessionId,
|
|
1364
|
-
workspace_authority:
|
|
1365
|
-
...(materialization
|
|
1366
|
-
? {
|
|
1367
|
-
workspace_marker_version: 1,
|
|
1368
|
-
workspace_continuity_state: materialization.workspaceContinuityState,
|
|
1369
|
-
workspace_snapshot_id: materialization.snapshotId,
|
|
1370
|
-
workspace_revision: materialization.revision,
|
|
1371
|
-
workspace_content_sha256: materialization.contentSha256,
|
|
1372
|
-
}
|
|
1373
|
-
: {}),
|
|
1374
|
-
...(session.workspaceCopyReceipt !== null
|
|
1375
|
-
? { workspace_copy_receipt: session.workspaceCopyReceipt }
|
|
1376
|
-
: {}),
|
|
1133
|
+
workspace_authority: "nas",
|
|
1377
1134
|
});
|
|
1378
1135
|
}
|
|
1379
1136
|
async handleWorkspaceCheckpoint(frame) {
|
|
@@ -1393,8 +1150,6 @@ export class AgentServiceSandboxClient {
|
|
|
1393
1150
|
"sandbox_pause",
|
|
1394
1151
|
"sandbox_cleanup",
|
|
1395
1152
|
"generation_rotation",
|
|
1396
|
-
"initial_migration",
|
|
1397
|
-
"live_workspace_copy",
|
|
1398
1153
|
"maintenance_repair",
|
|
1399
1154
|
].includes(String(sourceKind)) ||
|
|
1400
1155
|
!Number.isSafeInteger(baseRevision) ||
|
|
@@ -1422,7 +1177,7 @@ export class AgentServiceSandboxClient {
|
|
|
1422
1177
|
await checkpointWorkspace({
|
|
1423
1178
|
wsUrl: this.options.wsUrl,
|
|
1424
1179
|
reconnectToken: controlToken,
|
|
1425
|
-
workspaceDirectory: runtimeSessionWorkspaceDir(sessionId, this.sandboxGeneration),
|
|
1180
|
+
workspaceDirectory: runtimeSessionWorkspaceDir(session.workspaceId ?? sessionId, this.sandboxGeneration),
|
|
1426
1181
|
scope: {
|
|
1427
1182
|
checkpointId,
|
|
1428
1183
|
baseRevision,
|
|
@@ -1434,17 +1189,12 @@ export class AgentServiceSandboxClient {
|
|
|
1434
1189
|
freezeProof,
|
|
1435
1190
|
});
|
|
1436
1191
|
checkpointCommitted = true;
|
|
1437
|
-
|
|
1438
|
-
if (!marker)
|
|
1439
|
-
throw new Error("workspace checkpoint marker missing");
|
|
1192
|
+
// committed revision 只是派生投影,不改变运行目录,所以这里不上报物化标记。
|
|
1440
1193
|
await this.sendSessionFrame("session.opened", sessionId, {
|
|
1194
|
+
workspace_id: session.workspaceId,
|
|
1441
1195
|
workspace_ref: session.workspaceRef,
|
|
1442
1196
|
native_session_id: session.nativeSessionId,
|
|
1443
|
-
|
|
1444
|
-
workspace_continuity_state: marker.workspaceContinuityState,
|
|
1445
|
-
workspace_snapshot_id: marker.snapshotId,
|
|
1446
|
-
workspace_revision: marker.revision,
|
|
1447
|
-
workspace_content_sha256: marker.contentSha256,
|
|
1197
|
+
workspace_authority: "nas",
|
|
1448
1198
|
});
|
|
1449
1199
|
await this.sendCommandAck(frame, "ok");
|
|
1450
1200
|
if (!lifecycleCheckpoint)
|
|
@@ -1539,7 +1289,7 @@ export class AgentServiceSandboxClient {
|
|
|
1539
1289
|
if (required.length === 0)
|
|
1540
1290
|
return [];
|
|
1541
1291
|
if (!availableCapabilities) {
|
|
1542
|
-
const workspace = ensureRuntimeSessionDirectories(sessionId, this.sandboxGeneration);
|
|
1292
|
+
const workspace = ensureRuntimeSessionDirectories(sessionId, this.sandboxGeneration, session.workspaceId ?? sessionId);
|
|
1543
1293
|
availableCapabilities = new Set(availableRunCapabilities({
|
|
1544
1294
|
agent_run_id: "activation-probe",
|
|
1545
1295
|
course_run_id: session.courseRunId ?? "",
|
|
@@ -1622,7 +1372,7 @@ export class AgentServiceSandboxClient {
|
|
|
1622
1372
|
session.runtimeId = runtimeId;
|
|
1623
1373
|
session.contextRevision = contextRevision;
|
|
1624
1374
|
session.activationId = activationId;
|
|
1625
|
-
exposeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration);
|
|
1375
|
+
exposeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration, session.workspaceId ?? sessionId);
|
|
1626
1376
|
this.activationContexts.set(sessionId, {
|
|
1627
1377
|
activationId,
|
|
1628
1378
|
runtimeEnv,
|
|
@@ -1793,8 +1543,8 @@ export class AgentServiceSandboxClient {
|
|
|
1793
1543
|
await this.finishTurn({ agent_run_id: runId });
|
|
1794
1544
|
}
|
|
1795
1545
|
/**
|
|
1796
|
-
* 幂等关闭一个 runtime session:终止其 runtime
|
|
1797
|
-
*
|
|
1546
|
+
* 幂等关闭一个 runtime session:终止其 runtime 子进程并清空 session-local state。
|
|
1547
|
+
* Durable Workspace 由独立身份拥有,关闭执行上下文不会删除学员文件。
|
|
1798
1548
|
*/
|
|
1799
1549
|
async closeSession(sessionId, reason, sourceFrame, fence) {
|
|
1800
1550
|
const session = this.state.sessions[sessionId];
|
|
@@ -1809,7 +1559,11 @@ export class AgentServiceSandboxClient {
|
|
|
1809
1559
|
}
|
|
1810
1560
|
await this.terminateSessionWriters(session);
|
|
1811
1561
|
session.activationId = null;
|
|
1812
|
-
|
|
1562
|
+
const workspaceId = session.workspaceId ?? sessionId;
|
|
1563
|
+
const sharedWorkspaceIsOpen = Object.entries(this.state.sessions).some(([otherSessionId, other]) => otherSessionId !== sessionId && (other.workspaceId ?? otherSessionId) === workspaceId);
|
|
1564
|
+
if (!sharedWorkspaceIsOpen) {
|
|
1565
|
+
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration, workspaceId);
|
|
1566
|
+
}
|
|
1813
1567
|
if (fence && !this.isCurrentLifecycleFence(fence))
|
|
1814
1568
|
return;
|
|
1815
1569
|
if (this.activeSessionId === sessionId)
|
|
@@ -1818,7 +1572,7 @@ export class AgentServiceSandboxClient {
|
|
|
1818
1572
|
this.currentTurnSessionId = null;
|
|
1819
1573
|
for (const runId of runIds)
|
|
1820
1574
|
this.turnScopes.delete(runId);
|
|
1821
|
-
removeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration);
|
|
1575
|
+
removeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration, true, workspaceId);
|
|
1822
1576
|
delete this.state.sessions[sessionId];
|
|
1823
1577
|
this.persist();
|
|
1824
1578
|
}
|
|
@@ -1960,7 +1714,7 @@ export class AgentServiceSandboxClient {
|
|
|
1960
1714
|
this.activationContexts.delete(sessionId);
|
|
1961
1715
|
this.sessionProfiles.delete(sessionId);
|
|
1962
1716
|
if (terminateWriters) {
|
|
1963
|
-
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration);
|
|
1717
|
+
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration, session.workspaceId ?? sessionId);
|
|
1964
1718
|
}
|
|
1965
1719
|
if (this.activeSessionId === sessionId)
|
|
1966
1720
|
this.activeSessionId = null;
|
|
@@ -2122,7 +1876,7 @@ export class AgentServiceSandboxClient {
|
|
|
2122
1876
|
}
|
|
2123
1877
|
this.workspaceFileReadInFlight = true;
|
|
2124
1878
|
try {
|
|
2125
|
-
const result = await readWorkspaceFile(runtimeSessionWorkspaceDir(sessionId, this.sandboxGeneration), relativePath, {
|
|
1879
|
+
const result = await readWorkspaceFile(runtimeSessionWorkspaceDir(this.state.sessions[sessionId]?.workspaceId ?? sessionId, this.sandboxGeneration), relativePath, {
|
|
2126
1880
|
maxBytes: maxBytes,
|
|
2127
1881
|
expectedSizeBytes: expectedSizeBytes,
|
|
2128
1882
|
expectedSha256,
|
|
@@ -3,7 +3,7 @@ export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.v4";
|
|
|
3
3
|
export declare const WORKSPACE_FILE_READ_BINARY_CAPABILITY: "workspace_file_read_binary_v1";
|
|
4
4
|
export declare const WORKSPACE_FILE_CHUNK_BYTES: number;
|
|
5
5
|
export declare const WORKSPACE_FILE_CHUNK_HEADER_BYTES = 29;
|
|
6
|
-
export type SandboxFrameType = "sandbox.hello" | "sandbox.sync" | "session.open" | "session.activate" | "turn.start" | "turn.cancel" | "session.close" | "sandbox.drain" | "sandbox.shutdown" | "event.ack" | "auth.rotate" | "workspace.file.read" | "workspace.checkpoint" | "workspace.thaw" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.open_failed" | "session.closed" | "turn.event" | "turn.file.report" | "workspace.file.result" | "sandbox.drained" | "sandbox.log" | "pong" | "protocol.error";
|
|
6
|
+
export type SandboxFrameType = "sandbox.hello" | "sandbox.sync" | "session.open" | "session.activate" | "turn.start" | "turn.cancel" | "session.close" | "sandbox.drain" | "sandbox.shutdown" | "event.ack" | "auth.rotate" | "workspace.file.read" | "workspace.checkpoint" | "workspace.thaw" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.open_failed" | "session.closed" | "activation.cleaned" | "turn.event" | "turn.file.report" | "workspace.file.result" | "sandbox.drained" | "sandbox.log" | "pong" | "protocol.error";
|
|
7
7
|
export declare class UnsupportedSandboxProtocolError extends Error {
|
|
8
8
|
readonly schemaVersion: unknown;
|
|
9
9
|
constructor(schemaVersion: unknown);
|