@botlearn-course/daemon 0.0.20-beta.1 → 0.0.20-beta.3
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 +6 -1
- package/dist/agent-service-sandbox.js +398 -28
- package/dist/agent-service-ws-protocol.d.ts +3 -3
- package/dist/agent-service-ws-protocol.js +5 -2
- package/dist/cli.js +19 -1
- package/dist/file-candidates.js +46 -39
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/run-dispatcher.d.ts +1 -1
- package/dist/run-dispatcher.js +1 -1
- package/dist/runtime-env.js +4 -4
- package/dist/runtime-quiescence.d.ts +19 -0
- package/dist/runtime-quiescence.js +110 -0
- 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-quota.d.ts +4 -0
- package/dist/workspace-quota.js +42 -0
- package/dist/workspace-restore.d.ts +9 -1
- package/dist/workspace-restore.js +30 -16
- package/dist/workspace-snapshot-control.d.ts +29 -0
- package/dist/workspace-snapshot-control.js +169 -0
- package/dist/workspace-snapshot-policy.d.ts +24 -0
- package/dist/workspace-snapshot-policy.js +45 -0
- package/dist/workspace-snapshot-staging.d.ts +2 -9
- package/dist/workspace-snapshot-staging.js +59 -25
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createReadStream, rmSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureDaemonHome } from "./auth-store.js";
|
|
4
|
+
import { writeWorkspaceMaterializationMarker } from "./workspace-materialization.js";
|
|
5
|
+
import { WORKSPACE_SNAPSHOT_POLICY_V1 } from "./workspace-snapshot-policy.js";
|
|
6
|
+
import { stageWorkspaceSnapshot } from "./workspace-snapshot-staging.js";
|
|
7
|
+
export class WorkspaceSnapshotControlError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
retryable;
|
|
10
|
+
constructor(code, retryable) {
|
|
11
|
+
super(code);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.retryable = retryable;
|
|
14
|
+
this.name = "WorkspaceSnapshotControlError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function controlBase(wsUrl) {
|
|
18
|
+
const url = new URL(wsUrl);
|
|
19
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
20
|
+
url.pathname = "/internal/v1/workspace-checkpoints";
|
|
21
|
+
url.search = "";
|
|
22
|
+
url.hash = "";
|
|
23
|
+
return url.toString().replace(/\/$/, "");
|
|
24
|
+
}
|
|
25
|
+
async function requestJson(url, token, body, signal) {
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${token}`,
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
signal,
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
let code = `workspace_snapshot_control_${response.status}`;
|
|
37
|
+
let explicitRetryable;
|
|
38
|
+
try {
|
|
39
|
+
const payload = await response.json();
|
|
40
|
+
const detail = payload.detail;
|
|
41
|
+
if (detail && typeof detail === "object" && !Array.isArray(detail)) {
|
|
42
|
+
const stable = detail.code;
|
|
43
|
+
if (typeof stable === "string" && /^workspace_[a-z0-9_]+$/u.test(stable))
|
|
44
|
+
code = stable;
|
|
45
|
+
const retryable = detail.retryable;
|
|
46
|
+
if (typeof retryable === "boolean")
|
|
47
|
+
explicitRetryable = retryable;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Content-free HTTP fallback remains stable and retry classification stays status-based.
|
|
52
|
+
}
|
|
53
|
+
throw new WorkspaceSnapshotControlError(code, explicitRetryable ?? (response.status >= 500 || response.status === 408 || response.status === 429 ||
|
|
54
|
+
response.status === 409));
|
|
55
|
+
}
|
|
56
|
+
return await response.json();
|
|
57
|
+
}
|
|
58
|
+
function fence(scope) {
|
|
59
|
+
return {
|
|
60
|
+
sandbox_id: scope.sandboxId,
|
|
61
|
+
sandbox_generation: scope.sandboxGeneration,
|
|
62
|
+
connection_epoch: scope.connectionEpoch,
|
|
63
|
+
runtime_session_id: scope.runtimeSessionId,
|
|
64
|
+
...(scope.agentRunId ? { agent_run_id: scope.agentRunId } : {}),
|
|
65
|
+
...(scope.workerAttempt ? { worker_attempt: scope.workerAttempt } : {}),
|
|
66
|
+
...(scope.activationId ? { activation_id: scope.activationId } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export async function checkpointWorkspace(options) {
|
|
70
|
+
const { scope } = options;
|
|
71
|
+
const signal = AbortSignal.timeout(WORKSPACE_SNAPSHOT_POLICY_V1.checkpointAttemptDeadlineMs);
|
|
72
|
+
const checkpointUrl = `${controlBase(options.wsUrl)}/${scope.checkpointId}`;
|
|
73
|
+
const controlDirectory = path.join(ensureDaemonHome(), "agent-service-sandboxes", scope.sandboxId, "workspace-control");
|
|
74
|
+
const staged = stageWorkspaceSnapshot({
|
|
75
|
+
workspaceDirectory: options.workspaceDirectory,
|
|
76
|
+
controlDirectory,
|
|
77
|
+
checkpointId: scope.checkpointId,
|
|
78
|
+
limits: WORKSPACE_SNAPSHOT_POLICY_V1.limits,
|
|
79
|
+
maxStabilityRetries: WORKSPACE_SNAPSHOT_POLICY_V1.maxStabilityRetries,
|
|
80
|
+
freezeProof: options.freezeProof,
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
const entrySet = {
|
|
84
|
+
schemaVersion: "agent-workspace-entry-set/1",
|
|
85
|
+
entries: staged.entrySet.entries,
|
|
86
|
+
};
|
|
87
|
+
const proposal = await requestJson(`${checkpointUrl}/proposal`, options.reconnectToken, {
|
|
88
|
+
fence: fence(scope),
|
|
89
|
+
policy_id: WORKSPACE_SNAPSHOT_POLICY_V1.policyId,
|
|
90
|
+
base_revision: scope.baseRevision,
|
|
91
|
+
entry_count: staged.entrySet.entries.length,
|
|
92
|
+
file_count: staged.entrySet.fileCount,
|
|
93
|
+
total_bytes: staged.entrySet.totalBytes,
|
|
94
|
+
content_sha256: staged.entrySet.contentSha256,
|
|
95
|
+
entry_set: entrySet,
|
|
96
|
+
}, signal);
|
|
97
|
+
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
|
+
return {
|
|
112
|
+
revision: proposal.revision,
|
|
113
|
+
snapshotId: proposal.snapshot_id,
|
|
114
|
+
contentSha256: proposal.content_sha256,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
for (let offset = 0; offset < proposal.missing_blob_ids.length; offset += 25) {
|
|
118
|
+
const requestedBlobIds = proposal.missing_blob_ids.slice(offset, offset + 25);
|
|
119
|
+
const grants = await requestJson(`${checkpointUrl}/upload-grants`, options.reconnectToken, { fence: fence(scope), blob_ids: requestedBlobIds }, signal);
|
|
120
|
+
if (grants.length !== requestedBlobIds.length) {
|
|
121
|
+
throw new Error("workspace_snapshot_upload_grant_count_mismatch");
|
|
122
|
+
}
|
|
123
|
+
for (const item of grants) {
|
|
124
|
+
const source = staged.files.find((file) => file.sha256 === item.sha256 && file.sizeBytes === item.size_bytes);
|
|
125
|
+
if (!source)
|
|
126
|
+
throw new Error("workspace_snapshot_upload_source_missing");
|
|
127
|
+
const response = await fetch(item.grant.url, {
|
|
128
|
+
method: item.grant.method,
|
|
129
|
+
headers: item.grant.headers,
|
|
130
|
+
body: createReadStream(source.stagingPath),
|
|
131
|
+
duplex: "half",
|
|
132
|
+
signal,
|
|
133
|
+
});
|
|
134
|
+
if (!response.ok)
|
|
135
|
+
throw new Error(`workspace_snapshot_upload_${response.status}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let committed = await requestJson(`${checkpointUrl}/complete`, options.reconnectToken, { fence: fence(scope), policy_id: WORKSPACE_SNAPSHOT_POLICY_V1.policyId }, signal);
|
|
139
|
+
while (committed.status === "pending") {
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
141
|
+
committed = await requestJson(`${checkpointUrl}/status`, options.reconnectToken, { fence: fence(scope) }, signal);
|
|
142
|
+
}
|
|
143
|
+
if (committed.status !== "committed" ||
|
|
144
|
+
committed.snapshot_id === null ||
|
|
145
|
+
committed.revision === null ||
|
|
146
|
+
committed.content_sha256 === null) {
|
|
147
|
+
throw new WorkspaceSnapshotControlError(committed.error_code || "workspace_snapshot_verification_failed", false);
|
|
148
|
+
}
|
|
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
|
+
return {
|
|
161
|
+
revision: committed.revision,
|
|
162
|
+
snapshotId: committed.snapshot_id,
|
|
163
|
+
contentSha256: committed.content_sha256,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
rmSync(staged.stagingDirectory, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol limits frozen with Course Service's WorkspaceSnapshotPolicyV1.
|
|
3
|
+
* Deployment may enable the capability, but must never override these values.
|
|
4
|
+
*/
|
|
5
|
+
export declare const WORKSPACE_SNAPSHOT_POLICY_V1: {
|
|
6
|
+
readonly policyId: "WorkspaceSnapshotPolicyV1";
|
|
7
|
+
readonly limits: {
|
|
8
|
+
maxPathSegmentBytes: number;
|
|
9
|
+
maxPathBytes: number;
|
|
10
|
+
maxDepth: number;
|
|
11
|
+
maxFileBytes: number;
|
|
12
|
+
maxFileCount: number;
|
|
13
|
+
maxTotalBytes: number;
|
|
14
|
+
maxEntrySetBytes: number;
|
|
15
|
+
};
|
|
16
|
+
readonly maxStabilityRetries: 2;
|
|
17
|
+
readonly checkpointAttemptDeadlineMs: 30000;
|
|
18
|
+
readonly requiredRestoreFreeBytes: number;
|
|
19
|
+
readonly sessionRetainedHardLimitBytes: number;
|
|
20
|
+
readonly checkpointReservationBytes: number;
|
|
21
|
+
readonly restorePageEntries: 25;
|
|
22
|
+
};
|
|
23
|
+
/** Wire representation validated exactly by Agent Service during the v0.3 handshake. */
|
|
24
|
+
export declare function workspaceSnapshotPolicyCapability(): Record<string, unknown>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { RUNTIME_WORKSPACE_INODE_QUOTA, RUNTIME_WORKSPACE_QUOTA_BYTES, } from "./workspace-quota.js";
|
|
2
|
+
/**
|
|
3
|
+
* Protocol limits frozen with Course Service's WorkspaceSnapshotPolicyV1.
|
|
4
|
+
* Deployment may enable the capability, but must never override these values.
|
|
5
|
+
*/
|
|
6
|
+
export const WORKSPACE_SNAPSHOT_POLICY_V1 = {
|
|
7
|
+
policyId: "WorkspaceSnapshotPolicyV1",
|
|
8
|
+
limits: {
|
|
9
|
+
maxPathSegmentBytes: 255,
|
|
10
|
+
maxPathBytes: 1024,
|
|
11
|
+
maxDepth: 32,
|
|
12
|
+
maxFileBytes: 50 * 1024 * 1024,
|
|
13
|
+
maxFileCount: 2_000,
|
|
14
|
+
maxTotalBytes: 512 * 1024 * 1024,
|
|
15
|
+
maxEntrySetBytes: 1024 * 1024,
|
|
16
|
+
},
|
|
17
|
+
maxStabilityRetries: 2,
|
|
18
|
+
checkpointAttemptDeadlineMs: 30_000,
|
|
19
|
+
requiredRestoreFreeBytes: 1024 * 1024,
|
|
20
|
+
sessionRetainedHardLimitBytes: 2 * 1024 * 1024 * 1024,
|
|
21
|
+
checkpointReservationBytes: 512 * 1024 * 1024,
|
|
22
|
+
restorePageEntries: 25,
|
|
23
|
+
};
|
|
24
|
+
/** Wire representation validated exactly by Agent Service during the v0.3 handshake. */
|
|
25
|
+
export function workspaceSnapshotPolicyCapability() {
|
|
26
|
+
return {
|
|
27
|
+
policy_id: WORKSPACE_SNAPSHOT_POLICY_V1.policyId,
|
|
28
|
+
max_path_segment_bytes: WORKSPACE_SNAPSHOT_POLICY_V1.limits.maxPathSegmentBytes,
|
|
29
|
+
max_path_bytes: WORKSPACE_SNAPSHOT_POLICY_V1.limits.maxPathBytes,
|
|
30
|
+
max_depth: WORKSPACE_SNAPSHOT_POLICY_V1.limits.maxDepth,
|
|
31
|
+
max_file_bytes: WORKSPACE_SNAPSHOT_POLICY_V1.limits.maxFileBytes,
|
|
32
|
+
max_file_count: WORKSPACE_SNAPSHOT_POLICY_V1.limits.maxFileCount,
|
|
33
|
+
max_total_bytes: WORKSPACE_SNAPSHOT_POLICY_V1.limits.maxTotalBytes,
|
|
34
|
+
max_entry_set_bytes: WORKSPACE_SNAPSHOT_POLICY_V1.limits.maxEntrySetBytes,
|
|
35
|
+
required_restore_free_bytes: WORKSPACE_SNAPSHOT_POLICY_V1.requiredRestoreFreeBytes,
|
|
36
|
+
session_retained_hard_limit_bytes: WORKSPACE_SNAPSHOT_POLICY_V1.sessionRetainedHardLimitBytes,
|
|
37
|
+
checkpoint_reservation_bytes: WORKSPACE_SNAPSHOT_POLICY_V1.checkpointReservationBytes,
|
|
38
|
+
restore_page_entries: WORKSPACE_SNAPSHOT_POLICY_V1.restorePageEntries,
|
|
39
|
+
max_stability_retries: WORKSPACE_SNAPSHOT_POLICY_V1.maxStabilityRetries,
|
|
40
|
+
checkpoint_attempt_deadline_ms: WORKSPACE_SNAPSHOT_POLICY_V1.checkpointAttemptDeadlineMs,
|
|
41
|
+
prepare_deadline_seconds: 15 * 60,
|
|
42
|
+
runtime_filesystem_quota_bytes: RUNTIME_WORKSPACE_QUOTA_BYTES,
|
|
43
|
+
runtime_inode_quota: RUNTIME_WORKSPACE_INODE_QUOTA,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WorkspaceWriterFreezeProof } from "./runtime-quiescence.js";
|
|
1
2
|
import { type CanonicalWorkspaceEntrySet, type WorkspaceEntrySetLimits } from "./workspace-entry-set.js";
|
|
2
3
|
export declare class WorkspaceSnapshotStagingError extends Error {
|
|
3
4
|
readonly code: string;
|
|
@@ -21,14 +22,6 @@ export interface WorkspaceSnapshotStagingOptions {
|
|
|
21
22
|
checkpointId: string;
|
|
22
23
|
limits: WorkspaceEntrySetLimits;
|
|
23
24
|
maxStabilityRetries: number;
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
export interface WorkspaceQuiescenceProof {
|
|
27
|
-
proofVersion: "workspace-quiescence-proof/1";
|
|
28
|
-
sandboxId: string;
|
|
29
|
-
sandboxGeneration: number;
|
|
30
|
-
runtimeSessionId: string;
|
|
31
|
-
checkpointId: string;
|
|
32
|
-
allWritersStopped: true;
|
|
25
|
+
freezeProof: WorkspaceWriterFreezeProof;
|
|
33
26
|
}
|
|
34
27
|
export declare function stageWorkspaceSnapshot(options: WorkspaceSnapshotStagingOptions): StagedWorkspaceSnapshot;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmodSync, closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, rmSync, writeSync, } from "node:fs";
|
|
2
|
+
import { chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, rmSync, writeSync, } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { isWorkspaceWriterFreezeProof, } from "./runtime-quiescence.js";
|
|
4
5
|
import { defaultCaseFold, validateWorkspaceEntrySet, WORKSPACE_ENTRY_SET_SCHEMA, } from "./workspace-entry-set.js";
|
|
5
6
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
7
|
const EXCLUDED_NAMES = new Set(["node_modules"]);
|
|
@@ -107,30 +108,52 @@ function fsyncDirectory(directory) {
|
|
|
107
108
|
closeSync(handle);
|
|
108
109
|
}
|
|
109
110
|
}
|
|
111
|
+
function descriptorPath(handle, fallback) {
|
|
112
|
+
if (existsSync("/proc/self/fd")) {
|
|
113
|
+
const value = path.join("/proc/self/fd", String(handle));
|
|
114
|
+
if (!existsSync(value))
|
|
115
|
+
fail("workspace_snapshot_fd_traversal_unavailable");
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
// Darwin has no procfs open-file path that supports relative child lookup. It is used
|
|
119
|
+
// only by local tests/BYOA; managed durable sandboxes are Linux and fail closed above.
|
|
120
|
+
return fallback;
|
|
121
|
+
}
|
|
110
122
|
function stageOnce(options, paths) {
|
|
111
123
|
const stagingDirectory = path.join(paths.checkpointRoot, randomUUID());
|
|
112
124
|
mkdirSync(stagingDirectory, { mode: 0o700 });
|
|
113
125
|
chmodSync(stagingDirectory, 0o700);
|
|
114
126
|
try {
|
|
115
|
-
const
|
|
116
|
-
|
|
127
|
+
const rootHandle = openSync(paths.workspace, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
128
|
+
const rootStats = fstatSync(rootHandle, { bigint: true });
|
|
129
|
+
if (!rootStats.isDirectory()) {
|
|
130
|
+
closeSync(rootHandle);
|
|
117
131
|
fail("workspace_snapshot_root_invalid");
|
|
132
|
+
}
|
|
118
133
|
const entries = [];
|
|
119
134
|
const files = [];
|
|
120
135
|
let fileCount = 0;
|
|
121
136
|
let totalBytes = 0;
|
|
122
|
-
|
|
123
|
-
|
|
137
|
+
let estimatedEntrySetBytes = Buffer.byteLength('{"schemaVersion":"agent-workspace-entry-set/1","entries":[]}', "utf8");
|
|
138
|
+
function appendEntry(entry) {
|
|
139
|
+
estimatedEntrySetBytes += Buffer.byteLength(JSON.stringify(entry), "utf8") + 1;
|
|
140
|
+
if (estimatedEntrySetBytes > options.limits.maxEntrySetBytes) {
|
|
141
|
+
fail("workspace_snapshot_limit_exceeded");
|
|
142
|
+
}
|
|
143
|
+
entries.push(entry);
|
|
144
|
+
}
|
|
145
|
+
function walk(sourceHandle, sourceFallback, destinationDirectory, segments) {
|
|
146
|
+
const directoryBefore = fstatSync(sourceHandle, { bigint: true });
|
|
124
147
|
assertSourceNode(directoryBefore, {
|
|
125
148
|
rootDevice: rootStats.dev,
|
|
126
149
|
expected: "directory",
|
|
127
150
|
});
|
|
151
|
+
const sourceDirectory = descriptorPath(sourceHandle, sourceFallback);
|
|
128
152
|
const children = readdirSync(sourceDirectory, {
|
|
129
|
-
withFileTypes: true,
|
|
130
153
|
encoding: "buffer",
|
|
131
|
-
}).sort((left, right) => Buffer.compare(left
|
|
154
|
+
}).sort((left, right) => Buffer.compare(left, right));
|
|
132
155
|
for (const child of children) {
|
|
133
|
-
const name = decodeName(child
|
|
156
|
+
const name = decodeName(child);
|
|
134
157
|
if (excludedName(name))
|
|
135
158
|
continue;
|
|
136
159
|
const relativeSegments = [...segments, name];
|
|
@@ -145,13 +168,21 @@ function stageOnce(options, paths) {
|
|
|
145
168
|
fail("workspace_snapshot_unsupported_file_type");
|
|
146
169
|
if (stats.isDirectory()) {
|
|
147
170
|
assertSourceNode(stats, { rootDevice: rootStats.dev, expected: "directory" });
|
|
171
|
+
const childHandle = openSync(source, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
172
|
+
const openedDirectory = fstatSync(childHandle, { bigint: true });
|
|
173
|
+
if (!sameIdentity(stats, openedDirectory)) {
|
|
174
|
+
closeSync(childHandle);
|
|
175
|
+
fail("workspace_snapshot_source_changed", true);
|
|
176
|
+
}
|
|
148
177
|
mkdirSync(destination, { mode: 0o700 });
|
|
149
178
|
chmodSync(destination, 0o700);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
179
|
+
appendEntry({ type: "directory", path: relativePath, mode: "0700" });
|
|
180
|
+
try {
|
|
181
|
+
walk(childHandle, source, destination, relativeSegments);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
closeSync(childHandle);
|
|
153
185
|
}
|
|
154
|
-
walk(source, destination, relativeSegments);
|
|
155
186
|
continue;
|
|
156
187
|
}
|
|
157
188
|
const copied = copyStableFile(source, destination, {
|
|
@@ -163,7 +194,7 @@ function stageOnce(options, paths) {
|
|
|
163
194
|
if (fileCount > options.limits.maxFileCount ||
|
|
164
195
|
totalBytes > options.limits.maxTotalBytes)
|
|
165
196
|
fail("workspace_snapshot_limit_exceeded");
|
|
166
|
-
|
|
197
|
+
appendEntry({
|
|
167
198
|
type: "file",
|
|
168
199
|
path: relativePath,
|
|
169
200
|
mode: copied.mode,
|
|
@@ -176,17 +207,19 @@ function stageOnce(options, paths) {
|
|
|
176
207
|
sizeBytes: copied.sizeBytes,
|
|
177
208
|
sha256: copied.sha256,
|
|
178
209
|
});
|
|
179
|
-
if (entries.length > options.limits.maxEntrySetBytes) {
|
|
180
|
-
fail("workspace_snapshot_limit_exceeded");
|
|
181
|
-
}
|
|
182
210
|
}
|
|
183
|
-
const directoryAfter =
|
|
211
|
+
const directoryAfter = fstatSync(sourceHandle, { bigint: true });
|
|
184
212
|
if (!sameIdentity(directoryBefore, directoryAfter)) {
|
|
185
213
|
fail("workspace_snapshot_source_changed", true);
|
|
186
214
|
}
|
|
187
215
|
fsyncDirectory(destinationDirectory);
|
|
188
216
|
}
|
|
189
|
-
|
|
217
|
+
try {
|
|
218
|
+
walk(rootHandle, paths.workspace, stagingDirectory, []);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
closeSync(rootHandle);
|
|
222
|
+
}
|
|
190
223
|
entries.sort((left, right) => Buffer.compare(Buffer.from(left.path, "utf8"), Buffer.from(right.path, "utf8")));
|
|
191
224
|
files.sort((left, right) => Buffer.compare(Buffer.from(left.path, "utf8"), Buffer.from(right.path, "utf8")));
|
|
192
225
|
const entrySet = validateWorkspaceEntrySet({ schemaVersion: WORKSPACE_ENTRY_SET_SCHEMA, entries }, options.limits);
|
|
@@ -200,13 +233,14 @@ function stageOnce(options, paths) {
|
|
|
200
233
|
export function stageWorkspaceSnapshot(options) {
|
|
201
234
|
if (!UUID.test(options.checkpointId))
|
|
202
235
|
fail("workspace_snapshot_checkpoint_invalid");
|
|
203
|
-
if (options.
|
|
204
|
-
|
|
205
|
-
!UUID.test(options.
|
|
206
|
-
options.
|
|
207
|
-
|
|
208
|
-
options.
|
|
209
|
-
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)
|
|
210
244
|
fail("workspace_snapshot_writer_isolation_unproven");
|
|
211
245
|
if (!Number.isSafeInteger(options.maxStabilityRetries) || options.maxStabilityRetries < 0) {
|
|
212
246
|
fail("workspace_snapshot_retry_limit_invalid");
|
package/package.json
CHANGED