@botlearn-course/daemon 0.0.17 → 0.0.18-beta.2
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 +4 -0
- package/dist/agent-service-sandbox.js +100 -2
- package/dist/agent-service-ws-protocol.d.ts +11 -1
- package/dist/agent-service-ws-protocol.js +50 -0
- package/dist/input-attachments.d.ts +5 -0
- package/dist/input-attachments.js +65 -31
- package/dist/run-dispatcher.js +9 -3
- package/dist/runtimes/deepseek-tui.js +4 -0
- package/dist/websocket-client.d.ts +3 -3
- package/dist/websocket-client.js +7 -4
- package/dist/workspace-file-read.d.ts +16 -0
- package/dist/workspace-file-read.js +95 -0
- package/package.json +1 -1
|
@@ -64,6 +64,7 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
64
64
|
private runtimeLogSuppressedEvents;
|
|
65
65
|
private runtimeLogSuppressedBytes;
|
|
66
66
|
private runtimeLogSendChain;
|
|
67
|
+
private workspaceFileReadInFlight;
|
|
67
68
|
private readonly removeOperationalLogSink;
|
|
68
69
|
constructor(options: AgentServiceSandboxOptions);
|
|
69
70
|
prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
|
|
@@ -109,9 +110,12 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
|
|
|
109
110
|
private replaySpool;
|
|
110
111
|
private sendHeartbeat;
|
|
111
112
|
private sendCommandAck;
|
|
113
|
+
private handleWorkspaceFileRead;
|
|
114
|
+
private sendWorkspaceFileResult;
|
|
112
115
|
private sendControlFrame;
|
|
113
116
|
private sendSessionFrame;
|
|
114
117
|
private sendFrame;
|
|
118
|
+
private sendBinaryFrame;
|
|
115
119
|
private nextOutboundSeq;
|
|
116
120
|
private waitForEventCapacity;
|
|
117
121
|
private waitForPendingEventAcks;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ensureDaemonHome } from "./auth-store.js";
|
|
4
|
-
import { AGENT_SERVICE_WS_SCHEMA, AGENT_SERVICE_WS_SUBPROTOCOL, createSandboxFrame, parseSandboxFrame, UnsupportedSandboxProtocolError, } from "./agent-service-ws-protocol.js";
|
|
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";
|
|
5
5
|
import { log as defaultLog, setOperationalLogSink, } from "./log.js";
|
|
6
6
|
import { availableRunCapabilities, runtimeSupportsCourseSkills, } from "./runtime-capabilities.js";
|
|
7
7
|
import { activationRuntimeEnv, runtimeChildEnv } from "./runtime-env.js";
|
|
8
8
|
import { redactSecretString } from "./redaction.js";
|
|
9
9
|
import { parseRuntimeSkillProviderGrantSet, prepareRuntimeSkillProvider, RuntimeSkillProviderError, } from "./runtime-skills.js";
|
|
10
10
|
import { RunDispatcher, } from "./run-dispatcher.js";
|
|
11
|
-
import { ensureRuntimeSessionDirectories, ensureRuntimeSessionWorkspace, exposeRuntimeSessionWorkspace, removeRuntimeSessionWorkspace, revokeRuntimeSessionWorkspace, } from "./workspace.js";
|
|
11
|
+
import { ensureRuntimeSessionDirectories, ensureRuntimeSessionWorkspace, exposeRuntimeSessionWorkspace, removeRuntimeSessionWorkspace, revokeRuntimeSessionWorkspace, runtimeSessionWorkspaceDir, } from "./workspace.js";
|
|
12
|
+
import { readWorkspaceFile, WorkspaceFileReadError } from "./workspace-file-read.js";
|
|
12
13
|
import { WebSocketClient, } from "./websocket-client.js";
|
|
13
14
|
const RECONNECT_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000, 30_000];
|
|
14
15
|
const MAX_SPOOL_FRAMES = 1024;
|
|
@@ -19,6 +20,7 @@ const INPUT_ATTACHMENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-
|
|
|
19
20
|
/** 出站 seq 基址:seq = connection_epoch * SEQ_EPOCH_BASE + n,跨重连单调(合同 §1.1)。 */
|
|
20
21
|
const SEQ_EPOCH_BASE = 1_000_000_000;
|
|
21
22
|
const RUNTIME_LOG_WINDOW_MS = 60_000;
|
|
23
|
+
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;
|
|
22
24
|
const DISABLED_RUNTIME_LOG_POLICY = {
|
|
23
25
|
enabled: false,
|
|
24
26
|
maxEventBytes: 4096,
|
|
@@ -368,6 +370,7 @@ export class AgentServiceSandboxClient {
|
|
|
368
370
|
runtimeLogSuppressedEvents = 0;
|
|
369
371
|
runtimeLogSuppressedBytes = 0;
|
|
370
372
|
runtimeLogSendChain = Promise.resolve();
|
|
373
|
+
workspaceFileReadInFlight = false;
|
|
371
374
|
removeOperationalLogSink;
|
|
372
375
|
constructor(options) {
|
|
373
376
|
this.options = options;
|
|
@@ -784,6 +787,7 @@ export class AgentServiceSandboxClient {
|
|
|
784
787
|
this.persist();
|
|
785
788
|
await this.sendControlFrame("sandbox.ready", {
|
|
786
789
|
protocol_versions: [AGENT_SERVICE_WS_SCHEMA],
|
|
790
|
+
capabilities: [WORKSPACE_FILE_READ_BINARY_CAPABILITY],
|
|
787
791
|
daemon_version: this.options.daemonVersion,
|
|
788
792
|
runtime_versions: {
|
|
789
793
|
course_daemon: this.options.daemonVersion,
|
|
@@ -906,6 +910,15 @@ export class AgentServiceSandboxClient {
|
|
|
906
910
|
await this.sendCommandAck(frame, "ok");
|
|
907
911
|
return;
|
|
908
912
|
}
|
|
913
|
+
case "workspace.file.read":
|
|
914
|
+
void this.handleWorkspaceFileRead(frame).catch((error) => {
|
|
915
|
+
this.log.warn("Agent Service workspace file read response failed", {
|
|
916
|
+
sandboxId: this.options.sandboxId,
|
|
917
|
+
runtimeSessionId: frame.runtime_session_id,
|
|
918
|
+
error: error instanceof Error ? redactSecretString(error.message) : "unexpected",
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
return;
|
|
909
922
|
case "sandbox.shutdown":
|
|
910
923
|
this.stop();
|
|
911
924
|
return;
|
|
@@ -1578,6 +1591,78 @@ export class AgentServiceSandboxClient {
|
|
|
1578
1591
|
...(error !== undefined ? { error } : {}),
|
|
1579
1592
|
});
|
|
1580
1593
|
}
|
|
1594
|
+
async handleWorkspaceFileRead(frame) {
|
|
1595
|
+
const transferId = frame.payload.transfer_id;
|
|
1596
|
+
const relativePath = frame.payload.path;
|
|
1597
|
+
const maxBytes = frame.payload.max_bytes;
|
|
1598
|
+
const expectedSizeBytes = frame.payload.expected_size_bytes;
|
|
1599
|
+
const expectedSha256 = frame.payload.expected_sha256;
|
|
1600
|
+
const sessionId = frame.runtime_session_id;
|
|
1601
|
+
const reject = async (errorCode) => {
|
|
1602
|
+
await this.sendWorkspaceFileResult(frame, {
|
|
1603
|
+
transfer_id: typeof transferId === "string" ? transferId : "invalid",
|
|
1604
|
+
status: "rejected",
|
|
1605
|
+
error_code: errorCode,
|
|
1606
|
+
});
|
|
1607
|
+
};
|
|
1608
|
+
if (typeof transferId !== "string" ||
|
|
1609
|
+
!WORKSPACE_FILE_TRANSFER_ID.test(transferId) ||
|
|
1610
|
+
typeof relativePath !== "string" ||
|
|
1611
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
1612
|
+
!Number.isSafeInteger(expectedSizeBytes) ||
|
|
1613
|
+
typeof expectedSha256 !== "string") {
|
|
1614
|
+
await reject("workspace_file_invalid_request");
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (!this.state.sessions[sessionId]) {
|
|
1618
|
+
await reject("workspace_file_unavailable");
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
if (this.workspaceFileReadInFlight) {
|
|
1622
|
+
await reject("workspace_file_busy");
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
this.workspaceFileReadInFlight = true;
|
|
1626
|
+
try {
|
|
1627
|
+
const result = await readWorkspaceFile(runtimeSessionWorkspaceDir(sessionId, this.sandboxGeneration), relativePath, {
|
|
1628
|
+
maxBytes: maxBytes,
|
|
1629
|
+
expectedSizeBytes: expectedSizeBytes,
|
|
1630
|
+
expectedSha256,
|
|
1631
|
+
onChunk: async (chunkIndex, content) => {
|
|
1632
|
+
await this.sendBinaryFrame(createWorkspaceFileChunk(transferId, chunkIndex, content), frame);
|
|
1633
|
+
},
|
|
1634
|
+
});
|
|
1635
|
+
await this.sendWorkspaceFileResult(frame, {
|
|
1636
|
+
transfer_id: transferId,
|
|
1637
|
+
status: "ok",
|
|
1638
|
+
size_bytes: result.sizeBytes,
|
|
1639
|
+
sha256: result.sha256,
|
|
1640
|
+
chunk_count: result.chunkCount,
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
catch (error) {
|
|
1644
|
+
await reject(error instanceof WorkspaceFileReadError
|
|
1645
|
+
? error.code
|
|
1646
|
+
: "workspace_file_unavailable");
|
|
1647
|
+
}
|
|
1648
|
+
finally {
|
|
1649
|
+
this.workspaceFileReadInFlight = false;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
async sendWorkspaceFileResult(source, payload) {
|
|
1653
|
+
if (source.sandbox_generation !== this.sandboxGeneration ||
|
|
1654
|
+
source.connection_epoch !== this.connectionEpoch)
|
|
1655
|
+
return;
|
|
1656
|
+
await this.sendFrame(createSandboxFrame({
|
|
1657
|
+
type: "workspace.file.result",
|
|
1658
|
+
sandboxId: this.options.sandboxId,
|
|
1659
|
+
sandboxGeneration: this.sandboxGeneration,
|
|
1660
|
+
connectionEpoch: this.connectionEpoch,
|
|
1661
|
+
seq: this.nextOutboundSeq(),
|
|
1662
|
+
runtimeSessionId: source.runtime_session_id,
|
|
1663
|
+
payload,
|
|
1664
|
+
}));
|
|
1665
|
+
}
|
|
1581
1666
|
async sendControlFrame(type, payload) {
|
|
1582
1667
|
await this.sendFrame(createSandboxFrame({
|
|
1583
1668
|
type,
|
|
@@ -1611,6 +1696,19 @@ export class AgentServiceSandboxClient {
|
|
|
1611
1696
|
socket.send(JSON.stringify(frame), (error) => (error ? reject(error) : resolve()));
|
|
1612
1697
|
});
|
|
1613
1698
|
}
|
|
1699
|
+
async sendBinaryFrame(content, source) {
|
|
1700
|
+
if (source.sandbox_generation !== this.sandboxGeneration ||
|
|
1701
|
+
source.connection_epoch !== this.connectionEpoch) {
|
|
1702
|
+
throw new Error("Agent Service sandbox file transfer was fenced");
|
|
1703
|
+
}
|
|
1704
|
+
const socket = this.socket;
|
|
1705
|
+
if (!socket || socket.readyState !== WebSocketClient.OPEN) {
|
|
1706
|
+
throw new Error("Agent Service sandbox file transport is disconnected");
|
|
1707
|
+
}
|
|
1708
|
+
await new Promise((resolve, reject) => {
|
|
1709
|
+
socket.send(content, (error) => (error ? reject(error) : resolve()));
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1614
1712
|
nextOutboundSeq() {
|
|
1615
1713
|
this.outboundSeq += 1;
|
|
1616
1714
|
return this.outboundSeq;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.2";
|
|
2
2
|
export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.v2";
|
|
3
|
-
export
|
|
3
|
+
export declare const WORKSPACE_FILE_READ_BINARY_CAPABILITY: "workspace_file_read_binary_v1";
|
|
4
|
+
export declare const WORKSPACE_FILE_CHUNK_BYTES: number;
|
|
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" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.closed" | "turn.event" | "turn.file.report" | "workspace.file.result" | "sandbox.drained" | "sandbox.log" | "pong" | "protocol.error";
|
|
4
7
|
export declare class UnsupportedSandboxProtocolError extends Error {
|
|
5
8
|
readonly schemaVersion: unknown;
|
|
6
9
|
constructor(schemaVersion: unknown);
|
|
@@ -20,6 +23,13 @@ export interface SandboxFrame {
|
|
|
20
23
|
activation_id: string | null;
|
|
21
24
|
payload: Record<string, unknown>;
|
|
22
25
|
}
|
|
26
|
+
export interface WorkspaceFileChunk {
|
|
27
|
+
transferId: string;
|
|
28
|
+
chunkIndex: number;
|
|
29
|
+
content: Buffer;
|
|
30
|
+
}
|
|
31
|
+
export declare function createWorkspaceFileChunk(transferId: string, chunkIndex: number, content: Buffer): Buffer;
|
|
32
|
+
export declare function parseWorkspaceFileChunk(raw: Buffer): WorkspaceFileChunk;
|
|
23
33
|
export declare function parseSandboxFrame(raw: string, maxBytes?: number): SandboxFrame;
|
|
24
34
|
export declare function createSandboxFrame(input: {
|
|
25
35
|
type: SandboxFrameType;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
export const AGENT_SERVICE_WS_SCHEMA = "botlearn-agent-sandbox-ws/0.2";
|
|
3
3
|
export const AGENT_SERVICE_WS_SUBPROTOCOL = "botlearn-agent-sandbox.v2";
|
|
4
|
+
export const WORKSPACE_FILE_READ_BINARY_CAPABILITY = "workspace_file_read_binary_v1";
|
|
5
|
+
export const WORKSPACE_FILE_CHUNK_BYTES = 64 * 1024;
|
|
6
|
+
export const WORKSPACE_FILE_CHUNK_HEADER_BYTES = 29;
|
|
7
|
+
const WORKSPACE_FILE_CHUNK_MAGIC = Buffer.from("BLWF", "ascii");
|
|
4
8
|
const FRAME_TYPES = new Set([
|
|
5
9
|
"sandbox.hello",
|
|
6
10
|
"sandbox.sync",
|
|
@@ -13,6 +17,7 @@ const FRAME_TYPES = new Set([
|
|
|
13
17
|
"sandbox.shutdown",
|
|
14
18
|
"event.ack",
|
|
15
19
|
"auth.rotate",
|
|
20
|
+
"workspace.file.read",
|
|
16
21
|
"ping",
|
|
17
22
|
"sandbox.ready",
|
|
18
23
|
"sandbox.heartbeat",
|
|
@@ -21,6 +26,7 @@ const FRAME_TYPES = new Set([
|
|
|
21
26
|
"session.closed",
|
|
22
27
|
"turn.event",
|
|
23
28
|
"turn.file.report",
|
|
29
|
+
"workspace.file.result",
|
|
24
30
|
"sandbox.drained",
|
|
25
31
|
"sandbox.log",
|
|
26
32
|
"pong",
|
|
@@ -33,6 +39,8 @@ const SESSION_TYPES = new Set([
|
|
|
33
39
|
"session.close",
|
|
34
40
|
"session.opened",
|
|
35
41
|
"session.closed",
|
|
42
|
+
"workspace.file.read",
|
|
43
|
+
"workspace.file.result",
|
|
36
44
|
]);
|
|
37
45
|
/** TURN 帧:必须带 runtime_session_id + agent_run_id + worker_attempt + activation_id 四元组。 */
|
|
38
46
|
const TURN_TYPES = new Set([
|
|
@@ -66,6 +74,48 @@ export class UnsupportedSandboxProtocolError extends Error {
|
|
|
66
74
|
this.name = "UnsupportedSandboxProtocolError";
|
|
67
75
|
}
|
|
68
76
|
}
|
|
77
|
+
function uuidBytes(value) {
|
|
78
|
+
if (!UUID_PATTERN.test(value))
|
|
79
|
+
throw new Error("workspace file transfer_id must be a UUID");
|
|
80
|
+
return Buffer.from(value.replaceAll("-", ""), "hex");
|
|
81
|
+
}
|
|
82
|
+
function bytesUuid(value) {
|
|
83
|
+
const hex = value.toString("hex");
|
|
84
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
85
|
+
}
|
|
86
|
+
export function createWorkspaceFileChunk(transferId, chunkIndex, content) {
|
|
87
|
+
if (!Number.isSafeInteger(chunkIndex) || chunkIndex < 0 || chunkIndex > 0xffff_ffff) {
|
|
88
|
+
throw new Error("workspace file chunk_index is invalid");
|
|
89
|
+
}
|
|
90
|
+
if (content.length > WORKSPACE_FILE_CHUNK_BYTES) {
|
|
91
|
+
throw new Error("workspace file chunk exceeds size limit");
|
|
92
|
+
}
|
|
93
|
+
const frame = Buffer.allocUnsafe(WORKSPACE_FILE_CHUNK_HEADER_BYTES + content.length);
|
|
94
|
+
WORKSPACE_FILE_CHUNK_MAGIC.copy(frame, 0);
|
|
95
|
+
frame[4] = 1;
|
|
96
|
+
uuidBytes(transferId).copy(frame, 5);
|
|
97
|
+
frame.writeUInt32BE(chunkIndex, 21);
|
|
98
|
+
frame.writeUInt32BE(content.length, 25);
|
|
99
|
+
content.copy(frame, WORKSPACE_FILE_CHUNK_HEADER_BYTES);
|
|
100
|
+
return frame;
|
|
101
|
+
}
|
|
102
|
+
export function parseWorkspaceFileChunk(raw) {
|
|
103
|
+
if (raw.length < WORKSPACE_FILE_CHUNK_HEADER_BYTES ||
|
|
104
|
+
!raw.subarray(0, 4).equals(WORKSPACE_FILE_CHUNK_MAGIC) ||
|
|
105
|
+
raw[4] !== 1) {
|
|
106
|
+
throw new Error("invalid workspace file binary frame");
|
|
107
|
+
}
|
|
108
|
+
const declaredBytes = raw.readUInt32BE(25);
|
|
109
|
+
const content = raw.subarray(WORKSPACE_FILE_CHUNK_HEADER_BYTES);
|
|
110
|
+
if (declaredBytes !== content.length || content.length > WORKSPACE_FILE_CHUNK_BYTES) {
|
|
111
|
+
throw new Error("invalid workspace file binary frame length");
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
transferId: bytesUuid(raw.subarray(5, 21)),
|
|
115
|
+
chunkIndex: raw.readUInt32BE(21),
|
|
116
|
+
content: Buffer.from(content),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
69
119
|
function positiveInteger(value, label) {
|
|
70
120
|
if (!Number.isInteger(value) || value < 1) {
|
|
71
121
|
throw new Error(`${label} must be a positive integer`);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { MaterializedInputAttachment, RunInputAttachment } from "./types.js";
|
|
2
|
+
export type InputAttachmentErrorCode = "attachment_download_failed" | "attachment_download_timeout" | "attachment_read_failed" | "attachment_verification_failed";
|
|
3
|
+
export declare class InputAttachmentMaterializationError extends Error {
|
|
4
|
+
readonly code: InputAttachmentErrorCode;
|
|
5
|
+
constructor(message: string, code: InputAttachmentErrorCode, options?: ErrorOptions);
|
|
6
|
+
}
|
|
2
7
|
export interface InputAttachmentGrant {
|
|
3
8
|
downloads: Array<{
|
|
4
9
|
attachmentId: string;
|
|
@@ -10,6 +10,20 @@ const ATTACHMENT_DOWNLOAD_IDLE_TIMEOUT_MS = 60_000;
|
|
|
10
10
|
const ATTACHMENT_DOWNLOAD_MAX_DURATION_MS = 10 * 60_000;
|
|
11
11
|
const ATTACHMENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12
12
|
const SHA256 = /^[0-9a-f]{64}$/i;
|
|
13
|
+
export class InputAttachmentMaterializationError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
constructor(message, code, options) {
|
|
16
|
+
super(message, options);
|
|
17
|
+
this.name = "InputAttachmentMaterializationError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function attachmentVerificationError(message) {
|
|
22
|
+
return new InputAttachmentMaterializationError(message, "attachment_verification_failed");
|
|
23
|
+
}
|
|
24
|
+
function attachmentDownloadError(message) {
|
|
25
|
+
return new InputAttachmentMaterializationError(message, "attachment_download_failed");
|
|
26
|
+
}
|
|
13
27
|
const CONTENT_TYPE_EXTENSIONS = {
|
|
14
28
|
"image/png": ".png",
|
|
15
29
|
"image/jpeg": ".jpg",
|
|
@@ -42,43 +56,44 @@ function normalizedContentType(value) {
|
|
|
42
56
|
}
|
|
43
57
|
function validateAttachment(attachment) {
|
|
44
58
|
if (!ATTACHMENT_ID.test(attachment.attachment_id)) {
|
|
45
|
-
throw
|
|
59
|
+
throw attachmentVerificationError("invalid input attachment id");
|
|
46
60
|
}
|
|
47
61
|
if (typeof attachment.filename !== "string" ||
|
|
48
62
|
attachment.filename.length < 1 ||
|
|
49
63
|
attachment.filename.length > 255) {
|
|
50
|
-
throw
|
|
64
|
+
throw attachmentVerificationError("invalid input attachment filename");
|
|
51
65
|
}
|
|
52
66
|
const contentType = normalizedContentType(attachment.content_type);
|
|
53
67
|
const extension = CONTENT_TYPE_EXTENSIONS[contentType];
|
|
54
|
-
if (!extension)
|
|
55
|
-
throw
|
|
68
|
+
if (!extension) {
|
|
69
|
+
throw attachmentVerificationError(`unsupported input attachment type: ${contentType}`);
|
|
70
|
+
}
|
|
56
71
|
if (!Number.isInteger(attachment.size_bytes) ||
|
|
57
72
|
attachment.size_bytes < 1 ||
|
|
58
73
|
attachment.size_bytes > MAX_ATTACHMENT_BYTES) {
|
|
59
|
-
throw
|
|
74
|
+
throw attachmentVerificationError("invalid input attachment size");
|
|
60
75
|
}
|
|
61
76
|
if (!SHA256.test(attachment.sha256)) {
|
|
62
|
-
throw
|
|
77
|
+
throw attachmentVerificationError("invalid input attachment sha256");
|
|
63
78
|
}
|
|
64
79
|
return extension;
|
|
65
80
|
}
|
|
66
81
|
function validateDownloadUrl(value) {
|
|
67
82
|
if (!value || value.length > 4096) {
|
|
68
|
-
throw
|
|
83
|
+
throw attachmentDownloadError("invalid input attachment download URL");
|
|
69
84
|
}
|
|
70
85
|
let parsed;
|
|
71
86
|
try {
|
|
72
87
|
parsed = new URL(value);
|
|
73
88
|
}
|
|
74
89
|
catch {
|
|
75
|
-
throw
|
|
90
|
+
throw attachmentDownloadError("invalid input attachment download URL");
|
|
76
91
|
}
|
|
77
92
|
if (!["http:", "https:"].includes(parsed.protocol) ||
|
|
78
93
|
parsed.username ||
|
|
79
94
|
parsed.password ||
|
|
80
95
|
parsed.hash) {
|
|
81
|
-
throw
|
|
96
|
+
throw attachmentDownloadError("invalid input attachment download URL");
|
|
82
97
|
}
|
|
83
98
|
return value;
|
|
84
99
|
}
|
|
@@ -86,12 +101,12 @@ function validateGrant(grant) {
|
|
|
86
101
|
if (!Array.isArray(grant.downloads) ||
|
|
87
102
|
grant.downloads.length < 1 ||
|
|
88
103
|
grant.downloads.length > MAX_ATTACHMENTS) {
|
|
89
|
-
throw
|
|
104
|
+
throw attachmentDownloadError("invalid input attachment download grant");
|
|
90
105
|
}
|
|
91
106
|
const downloads = new Map();
|
|
92
107
|
for (const item of grant.downloads) {
|
|
93
108
|
if (!ATTACHMENT_ID.test(item.attachmentId) || downloads.has(item.attachmentId)) {
|
|
94
|
-
throw
|
|
109
|
+
throw attachmentDownloadError("invalid input attachment download grant");
|
|
95
110
|
}
|
|
96
111
|
downloads.set(item.attachmentId, validateDownloadUrl(item.downloadUrl));
|
|
97
112
|
}
|
|
@@ -133,15 +148,15 @@ async function verifyExisting(filePath, attachment) {
|
|
|
133
148
|
}
|
|
134
149
|
async function writeResponseToFile(response, partPath, attachment, signal, onDownloadProgress) {
|
|
135
150
|
if (!response.body)
|
|
136
|
-
throw
|
|
151
|
+
throw attachmentDownloadError("input attachment response has no body");
|
|
137
152
|
const declaredLength = response.headers.get("content-length");
|
|
138
153
|
if (declaredLength !== null && Number(declaredLength) !== attachment.size_bytes) {
|
|
139
|
-
throw
|
|
154
|
+
throw attachmentVerificationError("input attachment response size does not match metadata");
|
|
140
155
|
}
|
|
141
156
|
const responseType = normalizedContentType(response.headers.get("content-type") ?? "");
|
|
142
157
|
const expectedType = normalizedContentType(attachment.content_type);
|
|
143
158
|
if (responseType !== expectedType) {
|
|
144
|
-
throw
|
|
159
|
+
throw attachmentVerificationError("input attachment response type does not match metadata");
|
|
145
160
|
}
|
|
146
161
|
const digest = createHash("sha256");
|
|
147
162
|
const signatureChunks = [];
|
|
@@ -163,7 +178,7 @@ async function writeResponseToFile(response, partPath, attachment, signal, onDow
|
|
|
163
178
|
received += chunk.length;
|
|
164
179
|
onDownloadProgress?.();
|
|
165
180
|
if (received > attachment.size_bytes || received > MAX_ATTACHMENT_BYTES) {
|
|
166
|
-
throw
|
|
181
|
+
throw attachmentVerificationError("input attachment response exceeds declared size");
|
|
167
182
|
}
|
|
168
183
|
digest.update(chunk);
|
|
169
184
|
if (signatureBytes < 16) {
|
|
@@ -178,13 +193,13 @@ async function writeResponseToFile(response, partPath, attachment, signal, onDow
|
|
|
178
193
|
}
|
|
179
194
|
}
|
|
180
195
|
if (received !== attachment.size_bytes) {
|
|
181
|
-
throw
|
|
196
|
+
throw attachmentVerificationError("input attachment response size does not match metadata");
|
|
182
197
|
}
|
|
183
198
|
if (digest.digest("hex") !== attachment.sha256.toLowerCase()) {
|
|
184
|
-
throw
|
|
199
|
+
throw attachmentVerificationError("input attachment sha256 mismatch");
|
|
185
200
|
}
|
|
186
201
|
if (!hasExpectedImageSignature(expectedType, Buffer.concat(signatureChunks))) {
|
|
187
|
-
throw
|
|
202
|
+
throw attachmentVerificationError("input attachment file signature does not match content type");
|
|
188
203
|
}
|
|
189
204
|
await file.sync();
|
|
190
205
|
}
|
|
@@ -195,8 +210,8 @@ async function writeResponseToFile(response, partPath, attachment, signal, onDow
|
|
|
195
210
|
function startAttachmentDownloadBudget(parentSignal) {
|
|
196
211
|
const controller = new AbortController();
|
|
197
212
|
const abortFromParent = () => controller.abort(parentSignal.reason);
|
|
198
|
-
const abortStalledDownload = () => controller.abort(new
|
|
199
|
-
const abortOverlongDownload = () => controller.abort(new
|
|
213
|
+
const abortStalledDownload = () => controller.abort(new InputAttachmentMaterializationError("input attachment download stalled for 60s", "attachment_download_timeout"));
|
|
214
|
+
const abortOverlongDownload = () => controller.abort(new InputAttachmentMaterializationError("input attachment download exceeded the 600s maximum duration", "attachment_download_timeout"));
|
|
200
215
|
if (parentSignal.aborted) {
|
|
201
216
|
abortFromParent();
|
|
202
217
|
}
|
|
@@ -222,23 +237,23 @@ function startAttachmentDownloadBudget(parentSignal) {
|
|
|
222
237
|
},
|
|
223
238
|
};
|
|
224
239
|
}
|
|
225
|
-
|
|
240
|
+
async function materializeInputAttachmentsImplementation(workspaceDir, attachments, grant, signal) {
|
|
226
241
|
if (attachments.length > MAX_ATTACHMENTS) {
|
|
227
|
-
throw
|
|
242
|
+
throw attachmentVerificationError("too many input attachments");
|
|
228
243
|
}
|
|
229
244
|
if (new Set(attachments.map((item) => item.attachment_id)).size !== attachments.length) {
|
|
230
|
-
throw
|
|
245
|
+
throw attachmentVerificationError("duplicate input attachment id");
|
|
231
246
|
}
|
|
232
247
|
const extensions = new Map(attachments.map((attachment) => [attachment.attachment_id, validateAttachment(attachment)]));
|
|
233
248
|
const downloadUrls = validateGrant(grant);
|
|
234
249
|
if (downloadUrls.size !== attachments.length ||
|
|
235
250
|
attachments.some((attachment) => !downloadUrls.has(attachment.attachment_id))) {
|
|
236
|
-
throw
|
|
251
|
+
throw attachmentDownloadError("input attachment download grant does not match run attachments");
|
|
237
252
|
}
|
|
238
253
|
const inputDir = path.resolve(workspaceDir, ".botlearn", "input-attachments");
|
|
239
254
|
const workspaceRoot = path.resolve(workspaceDir);
|
|
240
255
|
if (!inputDir.startsWith(`${workspaceRoot}${path.sep}`)) {
|
|
241
|
-
throw
|
|
256
|
+
throw attachmentVerificationError("input attachment directory escapes workspace");
|
|
242
257
|
}
|
|
243
258
|
await mkdir(inputDir, { recursive: true, mode: 0o770 });
|
|
244
259
|
await chmod(path.dirname(inputDir), 0o770);
|
|
@@ -256,13 +271,22 @@ export async function materializeInputAttachments(workspaceDir, attachments, gra
|
|
|
256
271
|
const downloadUrl = downloadUrls.get(attachment.attachment_id);
|
|
257
272
|
const budget = startAttachmentDownloadBudget(signal);
|
|
258
273
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
274
|
+
let response;
|
|
275
|
+
try {
|
|
276
|
+
response = await fetch(downloadUrl, {
|
|
277
|
+
headers: { "accept-encoding": "identity" },
|
|
278
|
+
redirect: "error",
|
|
279
|
+
signal: budget.signal,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (budget.signal.reason instanceof InputAttachmentMaterializationError) {
|
|
284
|
+
throw budget.signal.reason;
|
|
285
|
+
}
|
|
286
|
+
throw new InputAttachmentMaterializationError("input attachment download request failed", "attachment_download_failed", { cause: error });
|
|
287
|
+
}
|
|
264
288
|
if (!response.ok) {
|
|
265
|
-
throw
|
|
289
|
+
throw attachmentDownloadError(`input attachment download failed with HTTP ${response.status}`);
|
|
266
290
|
}
|
|
267
291
|
await writeResponseToFile(response, partPath, attachment, budget.signal, budget.recordProgress);
|
|
268
292
|
await rename(partPath, filePath);
|
|
@@ -282,3 +306,13 @@ export async function materializeInputAttachments(workspaceDir, attachments, gra
|
|
|
282
306
|
}
|
|
283
307
|
return results;
|
|
284
308
|
}
|
|
309
|
+
export async function materializeInputAttachments(workspaceDir, attachments, grant, signal) {
|
|
310
|
+
try {
|
|
311
|
+
return await materializeInputAttachmentsImplementation(workspaceDir, attachments, grant, signal);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (error instanceof InputAttachmentMaterializationError)
|
|
315
|
+
throw error;
|
|
316
|
+
throw new InputAttachmentMaterializationError("input attachment could not be prepared", "attachment_read_failed", { cause: error });
|
|
317
|
+
}
|
|
318
|
+
}
|
package/dist/run-dispatcher.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { CourseClientError, isRunTerminal } from "./course-client.js";
|
|
3
3
|
import { reportFileCandidates } from "./file-candidates.js";
|
|
4
|
-
import { materializeInputAttachments, } from "./input-attachments.js";
|
|
4
|
+
import { InputAttachmentMaterializationError, materializeInputAttachments, } from "./input-attachments.js";
|
|
5
5
|
import { log as defaultLog } from "./log.js";
|
|
6
6
|
import { MAX_PROGRESS_EVENTS_PER_ATTEMPT, tryNormalizeProgressReport, } from "./mcp/report-progress.js";
|
|
7
7
|
import { errorInfo, redactSecretString, sanitizeRuntimeFailureText, truncateText, } from "./redaction.js";
|
|
@@ -444,7 +444,7 @@ export class RunDispatcher {
|
|
|
444
444
|
let inputAttachments = [];
|
|
445
445
|
if (requestedAttachments.length > 0) {
|
|
446
446
|
if (!persistentTurn?.inputAttachmentGrant) {
|
|
447
|
-
throw new
|
|
447
|
+
throw new InputAttachmentMaterializationError("input attachment download authorization is unavailable", "attachment_download_failed");
|
|
448
448
|
}
|
|
449
449
|
inputAttachments = await materializeInputAttachments(workspaceDir, requestedAttachments, persistentTurn.inputAttachmentGrant, controller.signal);
|
|
450
450
|
}
|
|
@@ -887,7 +887,13 @@ export class RunDispatcher {
|
|
|
887
887
|
}
|
|
888
888
|
else {
|
|
889
889
|
const info = errorInfo(err);
|
|
890
|
-
const errorType = err instanceof
|
|
890
|
+
const errorType = err instanceof RuntimeProfileApplyError
|
|
891
|
+
? err.code
|
|
892
|
+
: err instanceof InputAttachmentMaterializationError
|
|
893
|
+
? err.code
|
|
894
|
+
: err instanceof RuntimeExecutionError
|
|
895
|
+
? err.errorType
|
|
896
|
+
: "runtime_error";
|
|
891
897
|
await sendFailure(errorType, info.error_message, err, err instanceof RuntimeProfileApplyError
|
|
892
898
|
? { code: err.code, profile_apply_status: "failed" }
|
|
893
899
|
: {});
|
|
@@ -259,6 +259,10 @@ export class DeepseekTuiAdapter {
|
|
|
259
259
|
if (compacted.error) {
|
|
260
260
|
throw new Error(`native compaction failed: ${compacted.error}`);
|
|
261
261
|
}
|
|
262
|
+
// The marker arms exactly one compaction. Disarm it here so later turns
|
|
263
|
+
// start work directly; the post-turn threshold check re-arms when the
|
|
264
|
+
// compacted thread grows past the limit again.
|
|
265
|
+
this.clearCompactionRequired(opts);
|
|
262
266
|
}
|
|
263
267
|
}
|
|
264
268
|
runResult = await this.startTurnAndReadEvents({
|
|
@@ -7,8 +7,8 @@ export interface WebSocketClientOptions {
|
|
|
7
7
|
/**
|
|
8
8
|
* Narrow RFC 6455 client for the daemon control plane.
|
|
9
9
|
*
|
|
10
|
-
* It intentionally supports only text
|
|
11
|
-
* subprotocol. Keeping this transport on Node built-ins preserves the daemon's zero
|
|
10
|
+
* It intentionally supports only text, binary, and control frames, no extensions, and one
|
|
11
|
+
* configured subprotocol. Keeping this transport on Node built-ins preserves the daemon's zero
|
|
12
12
|
* production-dependency release invariant.
|
|
13
13
|
*/
|
|
14
14
|
export declare class WebSocketClient extends EventEmitter {
|
|
@@ -31,7 +31,7 @@ export declare class WebSocketClient extends EventEmitter {
|
|
|
31
31
|
private closeEmitted;
|
|
32
32
|
private closeSent;
|
|
33
33
|
constructor(rawUrl: string, protocol: string, options?: WebSocketClientOptions);
|
|
34
|
-
send(data: string, callback?: (error?: Error) => void): void;
|
|
34
|
+
send(data: string | Buffer, callback?: (error?: Error) => void): void;
|
|
35
35
|
close(code?: number, reason?: string): void;
|
|
36
36
|
private connect;
|
|
37
37
|
private sendHandshake;
|
package/dist/websocket-client.js
CHANGED
|
@@ -35,8 +35,8 @@ function framePayload(opcode, payload) {
|
|
|
35
35
|
/**
|
|
36
36
|
* Narrow RFC 6455 client for the daemon control plane.
|
|
37
37
|
*
|
|
38
|
-
* It intentionally supports only text
|
|
39
|
-
* subprotocol. Keeping this transport on Node built-ins preserves the daemon's zero
|
|
38
|
+
* It intentionally supports only text, binary, and control frames, no extensions, and one
|
|
39
|
+
* configured subprotocol. Keeping this transport on Node built-ins preserves the daemon's zero
|
|
40
40
|
* production-dependency release invariant.
|
|
41
41
|
*/
|
|
42
42
|
export class WebSocketClient extends EventEmitter {
|
|
@@ -77,12 +77,15 @@ export class WebSocketClient extends EventEmitter {
|
|
|
77
77
|
callback?.(new Error("WebSocket is not open"));
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
|
-
const
|
|
80
|
+
const binary = Buffer.isBuffer(data);
|
|
81
|
+
const payload = binary ? data : Buffer.from(data, "utf8");
|
|
81
82
|
if (payload.length > this.maxPayload) {
|
|
82
83
|
callback?.(new Error("WebSocket payload exceeds configured limit"));
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
|
-
this.socket.write(framePayload(0x1, payload), (error) =>
|
|
86
|
+
this.socket.write(framePayload(binary ? 0x2 : 0x1, payload), (error) => {
|
|
87
|
+
callback?.(error ?? undefined);
|
|
88
|
+
});
|
|
86
89
|
}
|
|
87
90
|
close(code = 1000, reason = "") {
|
|
88
91
|
if (this.readyState === CLOSED)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type WorkspaceFileReadErrorCode = "workspace_file_invalid_request" | "workspace_file_unavailable" | "workspace_file_changed";
|
|
2
|
+
export declare class WorkspaceFileReadError extends Error {
|
|
3
|
+
readonly code: WorkspaceFileReadErrorCode;
|
|
4
|
+
constructor(code: WorkspaceFileReadErrorCode);
|
|
5
|
+
}
|
|
6
|
+
export interface WorkspaceFileReadResult {
|
|
7
|
+
sizeBytes: number;
|
|
8
|
+
sha256: string;
|
|
9
|
+
chunkCount: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function readWorkspaceFile(workspaceRoot: string, relativePath: string, options: {
|
|
12
|
+
maxBytes: number;
|
|
13
|
+
expectedSizeBytes: number;
|
|
14
|
+
expectedSha256: string;
|
|
15
|
+
onChunk(chunkIndex: number, content: Buffer): Promise<void>;
|
|
16
|
+
}): Promise<WorkspaceFileReadResult>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { WORKSPACE_FILE_CHUNK_BYTES } from "./agent-service-ws-protocol.js";
|
|
6
|
+
export class WorkspaceFileReadError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code) {
|
|
9
|
+
super(code);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = "WorkspaceFileReadError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function relativePathParts(relativePath) {
|
|
15
|
+
if (!relativePath || path.isAbsolute(relativePath) || relativePath.includes("\\")) {
|
|
16
|
+
throw new WorkspaceFileReadError("workspace_file_invalid_request");
|
|
17
|
+
}
|
|
18
|
+
const parts = relativePath.split("/");
|
|
19
|
+
if (parts.some((part) => !part || part === "." || part === "..")) {
|
|
20
|
+
throw new WorkspaceFileReadError("workspace_file_invalid_request");
|
|
21
|
+
}
|
|
22
|
+
return parts;
|
|
23
|
+
}
|
|
24
|
+
async function resolveRegularWorkspaceFile(workspaceRoot, relativePath) {
|
|
25
|
+
const parts = relativePathParts(relativePath);
|
|
26
|
+
const resolvedRoot = await realpath(workspaceRoot).catch(() => {
|
|
27
|
+
throw new WorkspaceFileReadError("workspace_file_unavailable");
|
|
28
|
+
});
|
|
29
|
+
let candidate = resolvedRoot;
|
|
30
|
+
for (const part of parts) {
|
|
31
|
+
candidate = path.join(candidate, part);
|
|
32
|
+
const metadata = await lstat(candidate).catch(() => {
|
|
33
|
+
throw new WorkspaceFileReadError("workspace_file_unavailable");
|
|
34
|
+
});
|
|
35
|
+
if (metadata.isSymbolicLink()) {
|
|
36
|
+
throw new WorkspaceFileReadError("workspace_file_unavailable");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const resolved = await realpath(candidate).catch(() => {
|
|
40
|
+
throw new WorkspaceFileReadError("workspace_file_unavailable");
|
|
41
|
+
});
|
|
42
|
+
if (resolved === resolvedRoot || !resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
43
|
+
throw new WorkspaceFileReadError("workspace_file_unavailable");
|
|
44
|
+
}
|
|
45
|
+
return resolved;
|
|
46
|
+
}
|
|
47
|
+
export async function readWorkspaceFile(workspaceRoot, relativePath, options) {
|
|
48
|
+
if (!Number.isSafeInteger(options.maxBytes) ||
|
|
49
|
+
options.maxBytes < 0 ||
|
|
50
|
+
!Number.isSafeInteger(options.expectedSizeBytes) ||
|
|
51
|
+
options.expectedSizeBytes < 0 ||
|
|
52
|
+
options.expectedSizeBytes > options.maxBytes ||
|
|
53
|
+
!/^[0-9a-f]{64}$/.test(options.expectedSha256)) {
|
|
54
|
+
throw new WorkspaceFileReadError("workspace_file_invalid_request");
|
|
55
|
+
}
|
|
56
|
+
const resolved = await resolveRegularWorkspaceFile(workspaceRoot, relativePath);
|
|
57
|
+
let handle;
|
|
58
|
+
try {
|
|
59
|
+
handle = await open(resolved, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
60
|
+
const metadata = await handle.stat();
|
|
61
|
+
if (!metadata.isFile())
|
|
62
|
+
throw new WorkspaceFileReadError("workspace_file_unavailable");
|
|
63
|
+
if (metadata.size !== options.expectedSizeBytes || metadata.size > options.maxBytes) {
|
|
64
|
+
throw new WorkspaceFileReadError("workspace_file_changed");
|
|
65
|
+
}
|
|
66
|
+
const digest = createHash("sha256");
|
|
67
|
+
let offset = 0;
|
|
68
|
+
let chunkCount = 0;
|
|
69
|
+
while (offset < metadata.size) {
|
|
70
|
+
const requested = Math.min(WORKSPACE_FILE_CHUNK_BYTES, metadata.size - offset);
|
|
71
|
+
const buffer = Buffer.allocUnsafe(requested);
|
|
72
|
+
const { bytesRead } = await handle.read(buffer, 0, requested, offset);
|
|
73
|
+
if (bytesRead < 1)
|
|
74
|
+
throw new WorkspaceFileReadError("workspace_file_changed");
|
|
75
|
+
const content = buffer.subarray(0, bytesRead);
|
|
76
|
+
digest.update(content);
|
|
77
|
+
await options.onChunk(chunkCount, content);
|
|
78
|
+
offset += bytesRead;
|
|
79
|
+
chunkCount += 1;
|
|
80
|
+
}
|
|
81
|
+
const sha256 = digest.digest("hex");
|
|
82
|
+
if (offset !== options.expectedSizeBytes || sha256 !== options.expectedSha256) {
|
|
83
|
+
throw new WorkspaceFileReadError("workspace_file_changed");
|
|
84
|
+
}
|
|
85
|
+
return { sizeBytes: offset, sha256, chunkCount };
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error instanceof WorkspaceFileReadError)
|
|
89
|
+
throw error;
|
|
90
|
+
throw new WorkspaceFileReadError("workspace_file_unavailable");
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await handle?.close().catch(() => undefined);
|
|
94
|
+
}
|
|
95
|
+
}
|
package/package.json
CHANGED