@botlearn-course/daemon 0.0.20-beta.2 → 0.0.20-beta.4
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 +1 -2
- package/dist/agent-service-sandbox.js +67 -65
- package/dist/agent-service-ws-protocol.d.ts +3 -3
- package/dist/agent-service-ws-protocol.js +3 -2
- package/dist/file-candidates.js +46 -39
- package/dist/runtime-quiescence.d.ts +9 -6
- package/dist/runtime-quiescence.js +72 -4
- package/dist/tool-observation.d.ts +6 -5
- package/dist/tool-observation.js +6 -5
- package/dist/trace-projection.d.ts +1 -1
- package/dist/trace-projection.js +1 -1
- package/dist/workspace-snapshot-control.d.ts +2 -2
- package/dist/workspace-snapshot-control.js +1 -1
- package/dist/workspace-snapshot-staging.d.ts +2 -2
- package/dist/workspace-snapshot-staging.js +9 -9
- package/package.json +1 -1
|
@@ -33,8 +33,6 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
33
33
|
private readonly dispatcher;
|
|
34
34
|
/** agent_run_id → TURN scope(session/attempt/activation),事件与文件帧路由用。 */
|
|
35
35
|
private readonly turnScopes;
|
|
36
|
-
private readonly turnWorkspaceCheckpoints;
|
|
37
|
-
private readonly completedWorkspaceCheckpoints;
|
|
38
36
|
/** 每 run 内嵌 profile(turn payload 自带时优先)。 */
|
|
39
37
|
private readonly runProfiles;
|
|
40
38
|
/** activate 时装配的 per-session profile,turn.start 复用。 */
|
|
@@ -111,6 +109,7 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
111
109
|
private isCurrentLifecycleFence;
|
|
112
110
|
private runIdsForSession;
|
|
113
111
|
private deactivateSession;
|
|
112
|
+
private terminateSessionWriters;
|
|
114
113
|
private sameTurnScope;
|
|
115
114
|
private matchesTurnScope;
|
|
116
115
|
private assertTurnScope;
|
|
@@ -14,7 +14,7 @@ import { RunDispatcher, } from "./run-dispatcher.js";
|
|
|
14
14
|
import { cleanupRuntimeSessionWorkspaceCopyStaging, copyRuntimeSessionWorkspace, ensureRuntimeSessionDirectories, ensureRuntimeSessionWorkspace, exposeRuntimeSessionWorkspace, finalizeRuntimeSessionWorkspaceCopy, removeRuntimeSessionWorkspace, revokeRuntimeSessionWorkspace, runtimeSessionWorkspaceDir, WORKSPACE_COPY_POLICY_V1, WorkspaceCopyError, workspaceCopyV1Supported, } from "./workspace.js";
|
|
15
15
|
import { readWorkspaceFile, WorkspaceFileReadError } from "./workspace-file-read.js";
|
|
16
16
|
import { checkpointWorkspace, WorkspaceSnapshotControlError, } from "./workspace-snapshot-control.js";
|
|
17
|
-
import {
|
|
17
|
+
import { assertWorkspaceWriterFreezeSupported, freezeRuntimeWriters, issueWorkspaceWriterFreezeProof, quiesceRuntimeWriters, thawRuntimeWriters, } from "./runtime-quiescence.js";
|
|
18
18
|
import { WorkspaceSnapshotStagingError } from "./workspace-snapshot-staging.js";
|
|
19
19
|
import { validateWorkspaceEntrySet, WorkspaceEntrySetError, } from "./workspace-entry-set.js";
|
|
20
20
|
import { WORKSPACE_SNAPSHOT_POLICY_V1, workspaceSnapshotPolicyCapability, } from "./workspace-snapshot-policy.js";
|
|
@@ -359,8 +359,6 @@ export class AgentServiceSandboxClient {
|
|
|
359
359
|
dispatcher;
|
|
360
360
|
/** agent_run_id → TURN scope(session/attempt/activation),事件与文件帧路由用。 */
|
|
361
361
|
turnScopes = new Map();
|
|
362
|
-
turnWorkspaceCheckpoints = new Map();
|
|
363
|
-
completedWorkspaceCheckpoints = new Set();
|
|
364
362
|
/** 每 run 内嵌 profile(turn payload 自带时优先)。 */
|
|
365
363
|
runProfiles = new Map();
|
|
366
364
|
/** activate 时装配的 per-session profile,turn.start 复用。 */
|
|
@@ -514,15 +512,11 @@ export class AgentServiceSandboxClient {
|
|
|
514
512
|
const pending = session?.pendingActivationCleanup;
|
|
515
513
|
if (!session || !pending)
|
|
516
514
|
return;
|
|
517
|
-
await quiesceRuntimeWriters(pending.activationId);
|
|
518
|
-
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration);
|
|
519
515
|
const activation = this.activationContexts.get(sessionId);
|
|
520
516
|
if (activation?.activationId === pending.activationId) {
|
|
521
517
|
this.activationContexts.delete(sessionId);
|
|
522
518
|
this.sessionProfiles.delete(sessionId);
|
|
523
519
|
}
|
|
524
|
-
if (session.activationId === pending.activationId)
|
|
525
|
-
session.activationId = null;
|
|
526
520
|
if (!session.completedActivations.includes(pending.activationId)) {
|
|
527
521
|
// Session-close/generation-rotate are the retention bounds. Never evict an
|
|
528
522
|
// activation tombstone while the session is alive: activation IDs are opaque, so
|
|
@@ -537,8 +531,6 @@ export class AgentServiceSandboxClient {
|
|
|
537
531
|
delete session.acceptedCommands[commandId];
|
|
538
532
|
session.pendingActivationCleanup = null;
|
|
539
533
|
this.turnScopes.delete(pending.agentRunId);
|
|
540
|
-
if (this.activeSessionId === sessionId)
|
|
541
|
-
this.activeSessionId = null;
|
|
542
534
|
if (this.currentTurnSessionId === sessionId)
|
|
543
535
|
this.currentTurnSessionId = null;
|
|
544
536
|
this.persist();
|
|
@@ -621,9 +613,8 @@ export class AgentServiceSandboxClient {
|
|
|
621
613
|
if (!this.sandboxGeneration || !this.connectionEpoch) {
|
|
622
614
|
throw new Error("Agent Service sandbox is not authenticated");
|
|
623
615
|
}
|
|
624
|
-
// Terminal truth is never delayed by object storage.
|
|
625
|
-
//
|
|
626
|
-
// re-delivers it as a session-scoped workspace.checkpoint command.
|
|
616
|
+
// Terminal truth is never delayed by object storage. Durable workspace checkpointing
|
|
617
|
+
// is a separate lifecycle command issued only for pause/cleanup/rotation.
|
|
627
618
|
if (event.type === "run.block") {
|
|
628
619
|
await this.waitForEventCapacity();
|
|
629
620
|
}
|
|
@@ -863,11 +854,13 @@ export class AgentServiceSandboxClient {
|
|
|
863
854
|
await cleanupRuntimeSessionWorkspaceCopyStaging();
|
|
864
855
|
this.persist();
|
|
865
856
|
assertRuntimeWorkspaceQuota(process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT ?? "");
|
|
857
|
+
assertWorkspaceWriterFreezeSupported();
|
|
866
858
|
await this.sendControlFrame("sandbox.ready", {
|
|
867
859
|
protocol_versions: [AGENT_SERVICE_WS_SCHEMA],
|
|
868
860
|
capabilities: [
|
|
869
861
|
WORKSPACE_FILE_READ_BINARY_CAPABILITY,
|
|
870
|
-
"
|
|
862
|
+
"workspace_writer_freeze_v1",
|
|
863
|
+
"workspace_snapshot_v2",
|
|
871
864
|
"workspace_restore_v1",
|
|
872
865
|
...(workspaceCopyV1Supported() ? ["workspace_copy_v1"] : []),
|
|
873
866
|
],
|
|
@@ -1006,6 +999,10 @@ export class AgentServiceSandboxClient {
|
|
|
1006
999
|
case "workspace.checkpoint":
|
|
1007
1000
|
this.scheduleLifecycle("workspace.checkpoint", () => this.handleWorkspaceCheckpoint(frame));
|
|
1008
1001
|
return;
|
|
1002
|
+
case "workspace.thaw":
|
|
1003
|
+
await thawRuntimeWriters();
|
|
1004
|
+
await this.sendCommandAck(frame, "ok");
|
|
1005
|
+
return;
|
|
1009
1006
|
case "sandbox.shutdown":
|
|
1010
1007
|
await this.stopGracefully();
|
|
1011
1008
|
return;
|
|
@@ -1033,6 +1030,9 @@ export class AgentServiceSandboxClient {
|
|
|
1033
1030
|
this.scheduleLifecycle("sandbox.sync.close", (fence) => this.closeSession(sessionId, "sync_close", frame, fence));
|
|
1034
1031
|
}
|
|
1035
1032
|
const state = frame.payload.state;
|
|
1033
|
+
if (frame.payload.thaw_runtime_writers === true) {
|
|
1034
|
+
await thawRuntimeWriters();
|
|
1035
|
+
}
|
|
1036
1036
|
if (state === "shutdown") {
|
|
1037
1037
|
await this.stopGracefully();
|
|
1038
1038
|
return;
|
|
@@ -1360,38 +1360,38 @@ export class AgentServiceSandboxClient {
|
|
|
1360
1360
|
const checkpointId = frame.payload.checkpoint_id;
|
|
1361
1361
|
const controlToken = frame.payload.control_token;
|
|
1362
1362
|
const baseRevision = Number(frame.payload.base_revision);
|
|
1363
|
-
const
|
|
1364
|
-
const workerAttempt = frame.payload.worker_attempt;
|
|
1365
|
-
const activationId = frame.payload.activation_id;
|
|
1366
|
-
const pendingCleanup = session?.pendingActivationCleanup;
|
|
1367
|
-
if (pendingCleanup &&
|
|
1368
|
-
pendingCleanup.agentRunId === agentRunId &&
|
|
1369
|
-
pendingCleanup.workerAttempt === workerAttempt &&
|
|
1370
|
-
pendingCleanup.activationId === activationId) {
|
|
1371
|
-
await this.completePendingActivationCleanup(sessionId);
|
|
1372
|
-
}
|
|
1363
|
+
const sourceKind = frame.payload.source_kind;
|
|
1373
1364
|
if (!session ||
|
|
1374
|
-
this.activeSessionId === sessionId ||
|
|
1375
1365
|
this.currentTurnSessionId === sessionId ||
|
|
1376
1366
|
typeof checkpointId !== "string" ||
|
|
1377
1367
|
typeof controlToken !== "string" ||
|
|
1378
1368
|
controlToken.length < 32 ||
|
|
1379
1369
|
frame.payload.policy_id !== "WorkspaceSnapshotPolicyV1" ||
|
|
1370
|
+
![
|
|
1371
|
+
"sandbox_pause",
|
|
1372
|
+
"sandbox_cleanup",
|
|
1373
|
+
"generation_rotation",
|
|
1374
|
+
"initial_migration",
|
|
1375
|
+
"live_workspace_copy",
|
|
1376
|
+
"maintenance_repair",
|
|
1377
|
+
].includes(String(sourceKind)) ||
|
|
1380
1378
|
!Number.isSafeInteger(baseRevision) ||
|
|
1381
1379
|
baseRevision < 0 ||
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
Number(workerAttempt) >= 1 &&
|
|
1386
|
-
typeof activationId === "string" &&
|
|
1387
|
-
activationId.length > 0))) {
|
|
1380
|
+
frame.payload.agent_run_id != null ||
|
|
1381
|
+
frame.payload.worker_attempt != null ||
|
|
1382
|
+
frame.payload.activation_id != null) {
|
|
1388
1383
|
await this.sendCommandAck(frame, "rejected", "invalid_workspace_checkpoint");
|
|
1389
1384
|
return;
|
|
1390
1385
|
}
|
|
1386
|
+
const lifecycleCheckpoint = [
|
|
1387
|
+
"sandbox_pause",
|
|
1388
|
+
"sandbox_cleanup",
|
|
1389
|
+
"generation_rotation",
|
|
1390
|
+
].includes(String(sourceKind));
|
|
1391
|
+
let checkpointCommitted = false;
|
|
1391
1392
|
try {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const quiescenceProof = issueWorkspaceQuiescenceProof({
|
|
1393
|
+
await freezeRuntimeWriters();
|
|
1394
|
+
const freezeProof = issueWorkspaceWriterFreezeProof({
|
|
1395
1395
|
sandboxId: this.options.sandboxId,
|
|
1396
1396
|
sandboxGeneration: this.sandboxGeneration,
|
|
1397
1397
|
runtimeSessionId: sessionId,
|
|
@@ -1408,16 +1408,10 @@ export class AgentServiceSandboxClient {
|
|
|
1408
1408
|
sandboxGeneration: this.sandboxGeneration,
|
|
1409
1409
|
connectionEpoch: this.connectionEpoch,
|
|
1410
1410
|
runtimeSessionId: sessionId,
|
|
1411
|
-
...(typeof agentRunId === "string"
|
|
1412
|
-
? {
|
|
1413
|
-
agentRunId,
|
|
1414
|
-
workerAttempt: Number(workerAttempt),
|
|
1415
|
-
activationId: String(activationId),
|
|
1416
|
-
}
|
|
1417
|
-
: {}),
|
|
1418
1411
|
},
|
|
1419
|
-
|
|
1412
|
+
freezeProof,
|
|
1420
1413
|
});
|
|
1414
|
+
checkpointCommitted = true;
|
|
1421
1415
|
const marker = readWorkspaceMaterializationMarker(this.options.sandboxId, sessionId);
|
|
1422
1416
|
if (!marker)
|
|
1423
1417
|
throw new Error("workspace checkpoint marker missing");
|
|
@@ -1431,8 +1425,13 @@ export class AgentServiceSandboxClient {
|
|
|
1431
1425
|
workspace_content_sha256: marker.contentSha256,
|
|
1432
1426
|
});
|
|
1433
1427
|
await this.sendCommandAck(frame, "ok");
|
|
1428
|
+
if (!lifecycleCheckpoint)
|
|
1429
|
+
await thawRuntimeWriters();
|
|
1434
1430
|
}
|
|
1435
1431
|
catch (error) {
|
|
1432
|
+
if (!lifecycleCheckpoint || !checkpointCommitted) {
|
|
1433
|
+
await thawRuntimeWriters().catch(() => undefined);
|
|
1434
|
+
}
|
|
1436
1435
|
this.log.error("workspace baseline checkpoint failed", {
|
|
1437
1436
|
sandboxId: this.options.sandboxId,
|
|
1438
1437
|
runtimeSessionId: sessionId,
|
|
@@ -1583,7 +1582,7 @@ export class AgentServiceSandboxClient {
|
|
|
1583
1582
|
const previousSessionId = this.activeSessionId;
|
|
1584
1583
|
const changesActivation = previousSessionId !== null && (previousSessionId !== sessionId || session.activationId !== activationId);
|
|
1585
1584
|
if (changesActivation) {
|
|
1586
|
-
const stopped = await this.deactivateSession(previousSessionId);
|
|
1585
|
+
const stopped = await this.deactivateSession(previousSessionId, previousSessionId !== sessionId);
|
|
1587
1586
|
if (fence && !this.isCurrentLifecycleFence(fence))
|
|
1588
1587
|
return;
|
|
1589
1588
|
if (!stopped) {
|
|
@@ -1633,21 +1632,6 @@ export class AgentServiceSandboxClient {
|
|
|
1633
1632
|
await this.sendCommandAck(frame, "rejected", "activation_mismatch");
|
|
1634
1633
|
return;
|
|
1635
1634
|
}
|
|
1636
|
-
const checkpoint = frame.payload.workspace_checkpoint;
|
|
1637
|
-
if (checkpoint !== undefined) {
|
|
1638
|
-
if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint) ||
|
|
1639
|
-
typeof checkpoint.checkpoint_id !== "string" ||
|
|
1640
|
-
checkpoint.policy_id !== "WorkspaceSnapshotPolicyV1" ||
|
|
1641
|
-
!Number.isSafeInteger(checkpoint.base_revision) ||
|
|
1642
|
-
Number(checkpoint.base_revision) < 0) {
|
|
1643
|
-
await this.sendCommandAck(frame, "rejected", "invalid_workspace_checkpoint");
|
|
1644
|
-
return;
|
|
1645
|
-
}
|
|
1646
|
-
this.turnWorkspaceCheckpoints.set(runId, {
|
|
1647
|
-
checkpointId: String(checkpoint.checkpoint_id),
|
|
1648
|
-
baseRevision: Number(checkpoint.base_revision),
|
|
1649
|
-
});
|
|
1650
|
-
}
|
|
1651
1635
|
const payload = frame.payload;
|
|
1652
1636
|
if (!this.validTurnPayload(payload)) {
|
|
1653
1637
|
await this.sendCommandAck(frame, "rejected", "invalid_turn_payload");
|
|
@@ -1734,8 +1718,6 @@ export class AgentServiceSandboxClient {
|
|
|
1734
1718
|
.finally(() => {
|
|
1735
1719
|
this.inflightCommands.delete(commandId);
|
|
1736
1720
|
this.runProfiles.delete(runId);
|
|
1737
|
-
this.turnWorkspaceCheckpoints.delete(runId);
|
|
1738
|
-
this.completedWorkspaceCheckpoints.delete(runId);
|
|
1739
1721
|
});
|
|
1740
1722
|
}
|
|
1741
1723
|
async handleTurnCancel(frame) {
|
|
@@ -1796,15 +1778,16 @@ export class AgentServiceSandboxClient {
|
|
|
1796
1778
|
const session = this.state.sessions[sessionId];
|
|
1797
1779
|
if (session) {
|
|
1798
1780
|
const runIds = this.runIdsForSession(sessionId);
|
|
1799
|
-
session.activationId = null;
|
|
1800
1781
|
this.activationContexts.delete(sessionId);
|
|
1801
1782
|
this.sessionProfiles.delete(sessionId);
|
|
1802
|
-
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration);
|
|
1803
1783
|
for (const runId of runIds)
|
|
1804
1784
|
this.dispatcher.cancel(runId);
|
|
1805
1785
|
if (!await this.dispatcher.waitForRuns(runIds, 10_000)) {
|
|
1806
1786
|
throw new Error(`runtime session ${sessionId} did not stop before close`);
|
|
1807
1787
|
}
|
|
1788
|
+
await this.terminateSessionWriters(session);
|
|
1789
|
+
session.activationId = null;
|
|
1790
|
+
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration);
|
|
1808
1791
|
if (fence && !this.isCurrentLifecycleFence(fence))
|
|
1809
1792
|
return;
|
|
1810
1793
|
if (this.activeSessionId === sessionId)
|
|
@@ -1947,21 +1930,40 @@ export class AgentServiceSandboxClient {
|
|
|
1947
1930
|
.filter(([, scope]) => scope.sessionId === sessionId)
|
|
1948
1931
|
.map(([runId]) => runId);
|
|
1949
1932
|
}
|
|
1950
|
-
async deactivateSession(sessionId) {
|
|
1933
|
+
async deactivateSession(sessionId, terminateWriters = false) {
|
|
1951
1934
|
const session = this.state.sessions[sessionId];
|
|
1952
1935
|
if (!session)
|
|
1953
1936
|
return true;
|
|
1954
1937
|
const runIds = this.runIdsForSession(sessionId);
|
|
1955
|
-
session.activationId = null;
|
|
1956
1938
|
this.activationContexts.delete(sessionId);
|
|
1957
1939
|
this.sessionProfiles.delete(sessionId);
|
|
1958
|
-
|
|
1940
|
+
if (terminateWriters) {
|
|
1941
|
+
revokeRuntimeSessionWorkspace(sessionId, this.sandboxGeneration);
|
|
1942
|
+
}
|
|
1959
1943
|
if (this.activeSessionId === sessionId)
|
|
1960
1944
|
this.activeSessionId = null;
|
|
1961
1945
|
for (const runId of runIds)
|
|
1962
1946
|
this.dispatcher.cancel(runId);
|
|
1963
1947
|
this.persist();
|
|
1964
|
-
|
|
1948
|
+
const stopped = await this.dispatcher.waitForRuns(runIds, 10_000);
|
|
1949
|
+
if (stopped && terminateWriters) {
|
|
1950
|
+
await this.terminateSessionWriters(session);
|
|
1951
|
+
}
|
|
1952
|
+
session.activationId = null;
|
|
1953
|
+
this.persist();
|
|
1954
|
+
return stopped;
|
|
1955
|
+
}
|
|
1956
|
+
async terminateSessionWriters(session) {
|
|
1957
|
+
const activationIds = new Set([
|
|
1958
|
+
...session.completedActivations,
|
|
1959
|
+
...(session.activationId ? [session.activationId] : []),
|
|
1960
|
+
...(session.pendingActivationCleanup?.activationId
|
|
1961
|
+
? [session.pendingActivationCleanup.activationId]
|
|
1962
|
+
: []),
|
|
1963
|
+
]);
|
|
1964
|
+
for (const activationId of activationIds) {
|
|
1965
|
+
await quiesceRuntimeWriters(activationId);
|
|
1966
|
+
}
|
|
1965
1967
|
}
|
|
1966
1968
|
sameTurnScope(left, right) {
|
|
1967
1969
|
return left.sessionId === right.sessionId &&
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.
|
|
2
|
-
export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.
|
|
1
|
+
export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.4";
|
|
2
|
+
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" | "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" | "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);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
export const AGENT_SERVICE_WS_SCHEMA = "botlearn-agent-sandbox-ws/0.
|
|
3
|
-
export const AGENT_SERVICE_WS_SUBPROTOCOL = "botlearn-agent-sandbox.
|
|
2
|
+
export const AGENT_SERVICE_WS_SCHEMA = "botlearn-agent-sandbox-ws/0.4";
|
|
3
|
+
export const AGENT_SERVICE_WS_SUBPROTOCOL = "botlearn-agent-sandbox.v4";
|
|
4
4
|
export const WORKSPACE_FILE_READ_BINARY_CAPABILITY = "workspace_file_read_binary_v1";
|
|
5
5
|
export const WORKSPACE_FILE_CHUNK_BYTES = 64 * 1024;
|
|
6
6
|
export const WORKSPACE_FILE_CHUNK_HEADER_BYTES = 29;
|
|
@@ -19,6 +19,7 @@ const FRAME_TYPES = new Set([
|
|
|
19
19
|
"auth.rotate",
|
|
20
20
|
"workspace.file.read",
|
|
21
21
|
"workspace.checkpoint",
|
|
22
|
+
"workspace.thaw",
|
|
22
23
|
"ping",
|
|
23
24
|
"sandbox.ready",
|
|
24
25
|
"sandbox.heartbeat",
|
package/dist/file-candidates.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
3
|
import { open, readdir } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { performance } from "node:perf_hooks";
|
|
@@ -20,30 +20,50 @@ function isSafeRelativePath(rel) {
|
|
|
20
20
|
const segments = rel.split("/");
|
|
21
21
|
return segments.every((seg) => seg !== "" && seg !== "..");
|
|
22
22
|
}
|
|
23
|
-
async function sha256File(
|
|
23
|
+
async function sha256File(handle) {
|
|
24
24
|
const hash = createHash("sha256");
|
|
25
|
-
const stream = createReadStream(
|
|
25
|
+
const stream = handle.createReadStream({ autoClose: false, start: 0 });
|
|
26
26
|
for await (const chunk of stream)
|
|
27
27
|
hash.update(chunk);
|
|
28
28
|
return hash.digest("hex");
|
|
29
29
|
}
|
|
30
|
-
async function readPreview(
|
|
31
|
-
const
|
|
30
|
+
async function readPreview(handle, maxPreviewChars) {
|
|
31
|
+
const sniff = Buffer.alloc(TEXT_SNIFF_BYTES);
|
|
32
|
+
const { bytesRead } = await handle.read(sniff, 0, TEXT_SNIFF_BYTES, 0);
|
|
33
|
+
if (sniff.subarray(0, bytesRead).includes(0))
|
|
34
|
+
return null;
|
|
35
|
+
// 预览最多 maxPreviewChars 字符;UTF-8 下 4 字节/字符封顶,读够即可。
|
|
36
|
+
const want = maxPreviewChars * 4;
|
|
37
|
+
let buf = sniff.subarray(0, bytesRead);
|
|
38
|
+
if (bytesRead === TEXT_SNIFF_BYTES && want > TEXT_SNIFF_BYTES) {
|
|
39
|
+
const more = Buffer.alloc(want - TEXT_SNIFF_BYTES);
|
|
40
|
+
const extra = await handle.read(more, 0, more.length, TEXT_SNIFF_BYTES);
|
|
41
|
+
buf = Buffer.concat([buf, more.subarray(0, extra.bytesRead)]);
|
|
42
|
+
}
|
|
43
|
+
const text = buf.toString("utf8").replace(/�+$/, "");
|
|
44
|
+
return redactSecretString(text.slice(0, maxPreviewChars));
|
|
45
|
+
}
|
|
46
|
+
function sameFileVersion(left, right) {
|
|
47
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size &&
|
|
48
|
+
left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
49
|
+
}
|
|
50
|
+
async function scanStableFile(absPath, maxPreviewChars, maxFileBytes) {
|
|
51
|
+
const handle = await open(absPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
32
52
|
try {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return null;
|
|
37
|
-
// 预览最多 maxPreviewChars 字符;UTF-8 下 4 字节/字符封顶,读够即可。
|
|
38
|
-
const want = maxPreviewChars * 4;
|
|
39
|
-
let buf = sniff.subarray(0, bytesRead);
|
|
40
|
-
if (bytesRead === TEXT_SNIFF_BYTES && want > TEXT_SNIFF_BYTES) {
|
|
41
|
-
const more = Buffer.alloc(want - TEXT_SNIFF_BYTES);
|
|
42
|
-
const extra = await handle.read(more, 0, more.length, TEXT_SNIFF_BYTES);
|
|
43
|
-
buf = Buffer.concat([buf, more.subarray(0, extra.bytesRead)]);
|
|
53
|
+
const before = await handle.stat({ bigint: true });
|
|
54
|
+
if (!before.isFile() || before.size > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
55
|
+
throw new Error("workspace_candidate_invalid_file");
|
|
44
56
|
}
|
|
45
|
-
const
|
|
46
|
-
|
|
57
|
+
const sizeBytes = Number(before.size);
|
|
58
|
+
if (sizeBytes > maxFileBytes)
|
|
59
|
+
return { sizeBytes, tooLarge: true };
|
|
60
|
+
const sha256 = await sha256File(handle);
|
|
61
|
+
const previewText = await readPreview(handle, maxPreviewChars);
|
|
62
|
+
const after = await handle.stat({ bigint: true });
|
|
63
|
+
if (!sameFileVersion(before, after)) {
|
|
64
|
+
throw new Error("workspace_candidate_changed_during_scan");
|
|
65
|
+
}
|
|
66
|
+
return { sizeBytes, tooLarge: false, sha256, previewText };
|
|
47
67
|
}
|
|
48
68
|
finally {
|
|
49
69
|
await handle.close();
|
|
@@ -92,23 +112,18 @@ export async function scanWorkspaceFiles(workspaceDir, limits = {}) {
|
|
|
92
112
|
const rel = path.relative(workspaceDir, absPath).split(path.sep).join("/");
|
|
93
113
|
if (!isSafeRelativePath(rel))
|
|
94
114
|
continue;
|
|
95
|
-
let
|
|
115
|
+
let scanned;
|
|
96
116
|
try {
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
sizeBytes = (await handle.stat()).size;
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
await handle.close();
|
|
103
|
-
}
|
|
117
|
+
scanned = await scanStableFile(absPath, maxPreviewChars, maxFileBytes);
|
|
104
118
|
}
|
|
105
119
|
catch {
|
|
106
120
|
truncated = true;
|
|
107
121
|
continue;
|
|
108
122
|
}
|
|
123
|
+
const { sizeBytes } = scanned;
|
|
109
124
|
observedFileCount += 1;
|
|
110
125
|
observedTotalBytes += sizeBytes;
|
|
111
|
-
if (
|
|
126
|
+
if (scanned.tooLarge) {
|
|
112
127
|
truncated = true;
|
|
113
128
|
continue;
|
|
114
129
|
}
|
|
@@ -116,24 +131,16 @@ export async function scanWorkspaceFiles(workspaceDir, limits = {}) {
|
|
|
116
131
|
truncated = true;
|
|
117
132
|
continue;
|
|
118
133
|
}
|
|
119
|
-
let sha256;
|
|
120
|
-
let previewText;
|
|
121
|
-
try {
|
|
122
|
-
sha256 = await sha256File(absPath);
|
|
123
|
-
previewText = await readPreview(absPath, maxPreviewChars);
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
truncated = true;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
134
|
files.push({
|
|
130
135
|
absPath,
|
|
131
136
|
event: "created",
|
|
132
137
|
path: rel,
|
|
133
138
|
name: entry.name,
|
|
134
139
|
size_bytes: sizeBytes,
|
|
135
|
-
sha256,
|
|
136
|
-
...(previewText !== null && previewText !== ""
|
|
140
|
+
sha256: scanned.sha256,
|
|
141
|
+
...(scanned.previewText !== null && scanned.previewText !== ""
|
|
142
|
+
? { preview_text: scanned.previewText }
|
|
143
|
+
: {}),
|
|
137
144
|
});
|
|
138
145
|
}
|
|
139
146
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
readonly proofVersion: "workspace-
|
|
1
|
+
export interface WorkspaceWriterFreezeProof {
|
|
2
|
+
readonly proofVersion: "workspace-writer-freeze-proof/1";
|
|
3
3
|
readonly sandboxId: string;
|
|
4
4
|
readonly sandboxGeneration: number;
|
|
5
5
|
readonly runtimeSessionId: string;
|
|
6
6
|
readonly checkpointId: string;
|
|
7
|
-
readonly
|
|
7
|
+
readonly allWritersFrozen: true;
|
|
8
8
|
}
|
|
9
|
+
export declare function freezeRuntimeWriters(): Promise<void>;
|
|
10
|
+
export declare function thawRuntimeWriters(): Promise<void>;
|
|
11
|
+
export declare function assertWorkspaceWriterFreezeSupported(): void;
|
|
9
12
|
export declare function quiesceRuntimeWriters(activationId: string | null): Promise<void>;
|
|
10
|
-
export declare function
|
|
13
|
+
export declare function issueWorkspaceWriterFreezeProof(scope: {
|
|
11
14
|
sandboxId: string;
|
|
12
15
|
sandboxGeneration: number;
|
|
13
16
|
runtimeSessionId: string;
|
|
14
17
|
checkpointId: string;
|
|
15
|
-
}):
|
|
16
|
-
export declare function
|
|
18
|
+
}): WorkspaceWriterFreezeProof;
|
|
19
|
+
export declare function isWorkspaceWriterFreezeProof(value: object): value is WorkspaceWriterFreezeProof;
|
|
@@ -1,8 +1,76 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { accessSync, constants as fsConstants } from "node:fs";
|
|
3
|
+
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import { promisify } from "node:util";
|
|
3
6
|
const execFileAsync = promisify(execFile);
|
|
4
7
|
const RUNTIME_LAUNCHER = "/opt/botlearn/bin/botlearn-runtime-launcher";
|
|
8
|
+
const RUNTIME_CGROUP_ROOT = "/sys/fs/cgroup/botlearn-runtime";
|
|
5
9
|
const proofs = new WeakSet();
|
|
10
|
+
const sleep = (milliseconds) => new Promise((resolve) => {
|
|
11
|
+
setTimeout(resolve, milliseconds);
|
|
12
|
+
});
|
|
13
|
+
async function setDirectCgroupsFrozen(frozen) {
|
|
14
|
+
let entries;
|
|
15
|
+
try {
|
|
16
|
+
entries = await readdir(RUNTIME_CGROUP_ROOT, { withFileTypes: true });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === "ENOENT")
|
|
20
|
+
return;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (!entry.isDirectory())
|
|
25
|
+
continue;
|
|
26
|
+
const directory = path.join(RUNTIME_CGROUP_ROOT, entry.name);
|
|
27
|
+
await writeFile(path.join(directory, "cgroup.freeze"), frozen ? "1" : "0");
|
|
28
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
29
|
+
const events = await readFile(path.join(directory, "cgroup.events"), "utf8");
|
|
30
|
+
if (events.includes(`frozen ${frozen ? 1 : 0}`))
|
|
31
|
+
break;
|
|
32
|
+
if (attempt === 99)
|
|
33
|
+
throw new Error("workspace_runtime_freeze_timeout");
|
|
34
|
+
await sleep(50);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function setRuntimeWritersFrozen(frozen) {
|
|
39
|
+
const mode = process.env.BOTLEARN_RUNTIME_LAUNCH_MODE;
|
|
40
|
+
if (mode === "direct-uid") {
|
|
41
|
+
await setDirectCgroupsFrozen(frozen);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (mode === "sudo") {
|
|
45
|
+
await execFileAsync("/usr/bin/sudo", ["-n", "--", RUNTIME_LAUNCHER, frozen ? "--freeze-all" : "--thaw-all"], { timeout: 10_000 });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (process.env.BOTLEARN_DAEMON_ENABLE_FAKE_RUNTIME === "1" || process.env.NODE_ENV === "test") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
throw new Error("workspace_runtime_cgroup_unavailable");
|
|
52
|
+
}
|
|
53
|
+
export async function freezeRuntimeWriters() {
|
|
54
|
+
await setRuntimeWritersFrozen(true);
|
|
55
|
+
}
|
|
56
|
+
export async function thawRuntimeWriters() {
|
|
57
|
+
await setRuntimeWritersFrozen(false);
|
|
58
|
+
}
|
|
59
|
+
export function assertWorkspaceWriterFreezeSupported() {
|
|
60
|
+
const mode = process.env.BOTLEARN_RUNTIME_LAUNCH_MODE;
|
|
61
|
+
if (mode === "direct-uid") {
|
|
62
|
+
accessSync(RUNTIME_CGROUP_ROOT, fsConstants.R_OK | fsConstants.W_OK);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (mode === "sudo") {
|
|
66
|
+
accessSync(RUNTIME_LAUNCHER, fsConstants.X_OK);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (process.env.BOTLEARN_DAEMON_ENABLE_FAKE_RUNTIME === "1" || process.env.NODE_ENV === "test") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
throw new Error("workspace_runtime_cgroup_unavailable");
|
|
73
|
+
}
|
|
6
74
|
function managedQuiescenceCommand(activationId) {
|
|
7
75
|
const mode = process.env.BOTLEARN_RUNTIME_LAUNCH_MODE;
|
|
8
76
|
if (mode === "direct-uid") {
|
|
@@ -28,15 +96,15 @@ export async function quiesceRuntimeWriters(activationId) {
|
|
|
28
96
|
}
|
|
29
97
|
await execFileAsync(command[0], command[1], { timeout: 10_000 });
|
|
30
98
|
}
|
|
31
|
-
export function
|
|
99
|
+
export function issueWorkspaceWriterFreezeProof(scope) {
|
|
32
100
|
const proof = Object.freeze({
|
|
33
|
-
proofVersion: "workspace-
|
|
101
|
+
proofVersion: "workspace-writer-freeze-proof/1",
|
|
34
102
|
...scope,
|
|
35
|
-
|
|
103
|
+
allWritersFrozen: true,
|
|
36
104
|
});
|
|
37
105
|
proofs.add(proof);
|
|
38
106
|
return proof;
|
|
39
107
|
}
|
|
40
|
-
export function
|
|
108
|
+
export function isWorkspaceWriterFreezeProof(value) {
|
|
41
109
|
return proofs.has(value);
|
|
42
110
|
}
|
|
@@ -13,12 +13,13 @@ export interface ToolObservationPayload extends Record<string, unknown> {
|
|
|
13
13
|
redacted: boolean;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Project a provider tool envelope into durable
|
|
16
|
+
* Project a provider tool envelope into durable learner-preview and teaching-audit evidence.
|
|
17
17
|
*
|
|
18
18
|
* Only allowlisted operational argument/result fields survive, credentials are removed,
|
|
19
|
-
* and
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* and paths inside the current workspace become workspace-relative. Other semantic content,
|
|
20
|
+
* including execution-host paths outside that workspace, stays intact. `detail_preview` is
|
|
21
|
+
* a bounded feed-sized excerpt; `detail_full` carries the same allowlisted projection at
|
|
22
|
+
* audit fidelity (deeper walk, larger arrays, 64k chars). Message text, request IDs, and
|
|
23
|
+
* arbitrary envelope metadata never cross this boundary.
|
|
23
24
|
*/
|
|
24
25
|
export declare function buildToolObservation(block: RuntimeBlock, runtime: string, workspaceDir: string): ToolObservationPayload | null;
|
package/dist/tool-observation.js
CHANGED
|
@@ -39,13 +39,14 @@ const RESULT_FIELDS = new Set([
|
|
|
39
39
|
"bytes",
|
|
40
40
|
]);
|
|
41
41
|
/**
|
|
42
|
-
* Project a provider tool envelope into durable
|
|
42
|
+
* Project a provider tool envelope into durable learner-preview and teaching-audit evidence.
|
|
43
43
|
*
|
|
44
44
|
* Only allowlisted operational argument/result fields survive, credentials are removed,
|
|
45
|
-
* and
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* and paths inside the current workspace become workspace-relative. Other semantic content,
|
|
46
|
+
* including execution-host paths outside that workspace, stays intact. `detail_preview` is
|
|
47
|
+
* a bounded feed-sized excerpt; `detail_full` carries the same allowlisted projection at
|
|
48
|
+
* audit fidelity (deeper walk, larger arrays, 64k chars). Message text, request IDs, and
|
|
49
|
+
* arbitrary envelope metadata never cross this boundary.
|
|
49
50
|
*/
|
|
50
51
|
export function buildToolObservation(block, runtime, workspaceDir) {
|
|
51
52
|
if (block.kind !== "tool_call" && block.kind !== "tool_result")
|
|
@@ -17,5 +17,5 @@ export interface ReasoningTracePayload extends Record<string, unknown> {
|
|
|
17
17
|
* envelope stays local to the sandbox transcript.
|
|
18
18
|
*/
|
|
19
19
|
export declare function extractReasoningText(raw: unknown): string;
|
|
20
|
-
/** Project
|
|
20
|
+
/** Project reasoning into one bounded payload that filters credentials and preserves semantics. */
|
|
21
21
|
export declare function buildReasoningTracePayload(text: string, runtime: string, alreadyTruncated?: boolean): ReasoningTracePayload;
|
package/dist/trace-projection.js
CHANGED
|
@@ -40,7 +40,7 @@ function collectReasoningStrings(value, depth, out) {
|
|
|
40
40
|
collectReasoningStrings(item, depth + 1, out);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
/** Project
|
|
43
|
+
/** Project reasoning into one bounded payload that filters credentials and preserves semantics. */
|
|
44
44
|
export function buildReasoningTracePayload(text, runtime, alreadyTruncated = false) {
|
|
45
45
|
const redactedText = redactSecretString(text);
|
|
46
46
|
const truncated = alreadyTruncated || redactedText.length > REASONING_TRACE_MAX_CHARS;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { WorkspaceWriterFreezeProof } from "./runtime-quiescence.js";
|
|
2
2
|
interface WorkspaceCheckpointScope {
|
|
3
3
|
checkpointId: string;
|
|
4
4
|
baseRevision: number;
|
|
@@ -20,7 +20,7 @@ export declare function checkpointWorkspace(options: {
|
|
|
20
20
|
reconnectToken: string;
|
|
21
21
|
workspaceDirectory: string;
|
|
22
22
|
scope: WorkspaceCheckpointScope;
|
|
23
|
-
|
|
23
|
+
freezeProof: WorkspaceWriterFreezeProof;
|
|
24
24
|
}): Promise<{
|
|
25
25
|
revision: number;
|
|
26
26
|
snapshotId: string | null;
|
|
@@ -77,7 +77,7 @@ export async function checkpointWorkspace(options) {
|
|
|
77
77
|
checkpointId: scope.checkpointId,
|
|
78
78
|
limits: WORKSPACE_SNAPSHOT_POLICY_V1.limits,
|
|
79
79
|
maxStabilityRetries: WORKSPACE_SNAPSHOT_POLICY_V1.maxStabilityRetries,
|
|
80
|
-
|
|
80
|
+
freezeProof: options.freezeProof,
|
|
81
81
|
});
|
|
82
82
|
try {
|
|
83
83
|
const entrySet = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type WorkspaceWriterFreezeProof } from "./runtime-quiescence.js";
|
|
2
2
|
import { type CanonicalWorkspaceEntrySet, type WorkspaceEntrySetLimits } from "./workspace-entry-set.js";
|
|
3
3
|
export declare class WorkspaceSnapshotStagingError extends Error {
|
|
4
4
|
readonly code: string;
|
|
@@ -22,6 +22,6 @@ export interface WorkspaceSnapshotStagingOptions {
|
|
|
22
22
|
checkpointId: string;
|
|
23
23
|
limits: WorkspaceEntrySetLimits;
|
|
24
24
|
maxStabilityRetries: number;
|
|
25
|
-
|
|
25
|
+
freezeProof: WorkspaceWriterFreezeProof;
|
|
26
26
|
}
|
|
27
27
|
export declare function stageWorkspaceSnapshot(options: WorkspaceSnapshotStagingOptions): StagedWorkspaceSnapshot;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
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 {
|
|
4
|
+
import { isWorkspaceWriterFreezeProof, } from "./runtime-quiescence.js";
|
|
5
5
|
import { defaultCaseFold, validateWorkspaceEntrySet, WORKSPACE_ENTRY_SET_SCHEMA, } from "./workspace-entry-set.js";
|
|
6
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
7
|
const EXCLUDED_NAMES = new Set(["node_modules"]);
|
|
@@ -233,14 +233,14 @@ function stageOnce(options, paths) {
|
|
|
233
233
|
export function stageWorkspaceSnapshot(options) {
|
|
234
234
|
if (!UUID.test(options.checkpointId))
|
|
235
235
|
fail("workspace_snapshot_checkpoint_invalid");
|
|
236
|
-
if (!
|
|
237
|
-
options.
|
|
238
|
-
!UUID.test(options.
|
|
239
|
-
!UUID.test(options.
|
|
240
|
-
options.
|
|
241
|
-
!Number.isSafeInteger(options.
|
|
242
|
-
options.
|
|
243
|
-
options.
|
|
236
|
+
if (!isWorkspaceWriterFreezeProof(options.freezeProof) ||
|
|
237
|
+
options.freezeProof.proofVersion !== "workspace-writer-freeze-proof/1" ||
|
|
238
|
+
!UUID.test(options.freezeProof.sandboxId) ||
|
|
239
|
+
!UUID.test(options.freezeProof.runtimeSessionId) ||
|
|
240
|
+
options.freezeProof.checkpointId !== options.checkpointId ||
|
|
241
|
+
!Number.isSafeInteger(options.freezeProof.sandboxGeneration) ||
|
|
242
|
+
options.freezeProof.sandboxGeneration < 1 ||
|
|
243
|
+
options.freezeProof.allWritersFrozen !== true)
|
|
244
244
|
fail("workspace_snapshot_writer_isolation_unproven");
|
|
245
245
|
if (!Number.isSafeInteger(options.maxStabilityRetries) || options.maxStabilityRetries < 0) {
|
|
246
246
|
fail("workspace_snapshot_retry_limit_invalid");
|
package/package.json
CHANGED